predictor/internal/datasets/lock_unix.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):
}
}
}