-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
- Loading branch information
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
package flock | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
"os" | ||
"path/filepath" | ||
"strconv" | ||
"time" | ||
|
||
"golang.org/x/sys/unix" | ||
) | ||
|
||
var ErrLocked = errors.New("locked") | ||
|
||
// Acquire a lock on the given path. | ||
// | ||
// The lock is released when the returned function is called. | ||
func Acquire(ctx context.Context, path string, timeout time.Duration) (release func() error, err error) { | ||
absPath, err := filepath.Abs(path) | ||
if err != nil { | ||
return nil, err | ||
} | ||
end := time.Now().Add(timeout) | ||
for { | ||
release, err := acquire(absPath) | ||
if err == nil { | ||
return release, nil | ||
} | ||
if !errors.Is(err, ErrLocked) { | ||
return nil, fmt.Errorf("failed to acquire lock %s: %w", absPath, err) | ||
} | ||
if time.Now().After(end) { | ||
pid, _ := os.ReadFile(absPath) | ||
return nil, fmt.Errorf("timed out acquiring lock %s, locked by pid %s: %w", absPath, pid, err) | ||
} | ||
select { | ||
case <-ctx.Done(): | ||
return nil, ctx.Err() | ||
case <-time.After(time.Second): | ||
} | ||
} | ||
} | ||
|
||
func acquire(path string) (release func() error, err error) { | ||
pid := os.Getpid() | ||
fd, err := unix.Open(path, unix.O_CREAT|unix.O_RDWR|unix.O_CLOEXEC|unix.O_SYNC, 0600) | ||
if err != nil { | ||
return nil, fmt.Errorf("open failed: %w", err) | ||
} | ||
|
||
err = unix.Flock(fd, unix.LOCK_EX|unix.LOCK_NB) | ||
if err != nil { | ||
_ = unix.Close(fd) | ||
return nil, fmt.Errorf("%w: %w", ErrLocked, err) | ||
} | ||
|
||
_, err = unix.Write(fd, []byte(strconv.Itoa(pid))) | ||
if err != nil { | ||
return nil, fmt.Errorf("write failed: %w", err) | ||
} | ||
return func() error { | ||
return errors.Join(unix.Flock(fd, unix.LOCK_UN), unix.Close(fd), os.Remove(path)) | ||
}, nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package flock | ||
|
||
import ( | ||
"context" | ||
"path/filepath" | ||
"testing" | ||
|
||
"github.com/alecthomas/assert/v2" | ||
) | ||
|
||
func TestFlock(t *testing.T) { | ||
dir := t.TempDir() | ||
lockfile := filepath.Join(dir, "lock") | ||
ctx := context.Background() | ||
release, err := Acquire(ctx, lockfile, 0) | ||
assert.NoError(t, err) | ||
|
||
_, err = Acquire(ctx, lockfile, 0) | ||
assert.Error(t, err) | ||
|
||
err = release() | ||
assert.NoError(t, err) | ||
|
||
releaseb, err := Acquire(ctx, lockfile, 0) | ||
assert.NoError(t, err) | ||
err = releaseb() | ||
assert.NoError(t, err) | ||
} |