//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): } } }