50 lines
1.3 KiB
Go
50 lines
1.3 KiB
Go
//go:build unix
|
|
|
|
package datasets
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"syscall"
|
|
"time"
|
|
)
|
|
|
|
// lockPollInterval is how often a contended lock is retried. The lock is held
|
|
// for the duration of a dataset download (minutes), so sub-second acquisition
|
|
// latency is irrelevant.
|
|
const lockPollInterval = 150 * time.Millisecond
|
|
|
|
// flockExclusive acquires an exclusive flock on path, creating the lock file
|
|
// if needed, and blocks until it is held or ctx is cancelled.
|
|
//
|
|
// It uses non-blocking LOCK_NB attempts in a poll loop rather than a blocking
|
|
// flock in a goroutine: the file descriptor is only ever touched by this
|
|
// goroutine, so there is no race between a pending syscall and Close on
|
|
// cancellation.
|
|
func flockExclusive(ctx context.Context, path string) (func(), error) {
|
|
f, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0o644)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open lock file: %w", err)
|
|
}
|
|
for {
|
|
err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
|
|
if err == nil {
|
|
return func() {
|
|
_ = syscall.Flock(int(f.Fd()), syscall.LOCK_UN)
|
|
_ = f.Close()
|
|
}, nil
|
|
}
|
|
if !errors.Is(err, syscall.EWOULDBLOCK) {
|
|
f.Close()
|
|
return nil, fmt.Errorf("flock: %w", err)
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
f.Close()
|
|
return nil, ctx.Err()
|
|
case <-time.After(lockPollInterval):
|
|
}
|
|
}
|
|
}
|