| index dbccf7a4b..29a88713d 100644 |
| --- a/internal/host/movesfloor_linux.go |
| +++ b/internal/host/movesfloor_linux.go |
| @@ -109,24 +109,34 @@ func (d *movesDir) Lock() error { return d.lockNamed(floorLockFile, floorLockWai |
| // again on whatever is there now. The same removal can land inside the |
| // open itself (the name vanishes, or the file opened has no link left); |
| // that is retried too, while a symlink, a FIFO or a second link at the name |
| -// is still refused. Every retry is within the one wait: past it, the |
| -// call fails with busy and holds nothing. |
| +// is still refused. Each retry pauses (lockNamedRetryPause, capped at what |
| +// is left of the wait), and none starts once the wait has passed: the call |
| +// fails with busy then, an open race included, and holds nothing. |
| +// |
| +// The deadline bounds the wait; correctness does not depend on it. A flock |
| +// taken just after the deadline (the check and the syscall are not one |
| +// step) is still a valid, exclusive lock, and is kept. |
| func (d *movesDir) lockNamed(name string, wait time.Duration, busy error) error { |
| deadline := time.Now().Add(wait) |
| + timedOut := func() error { return fmt.Errorf("lock %s: %w", filepath.Join(d.path, name), busy) } |
| for { |
| - f, err := safefile.OpenFileAt(d.fd, name, os.O_RDONLY|os.O_CREATE, 0o644) |
| + if time.Now().After(deadline) { |
| + return timedOut() |
| + } |
| + f, err := lockNamedOpenAt(d.fd, name, os.O_RDONLY|os.O_CREATE, 0o644) |
| if err != nil { |
| - if time.Now().Before(deadline) && d.removedDuringOpen(name, err) { |
| - time.Sleep(floorLockPoll) |
| - continue |
| + if !d.removedDuringOpen(name, err) { |
| + return fmt.Errorf("open %s: %w", filepath.Join(d.path, name), err) |
| } |
| - return fmt.Errorf("open %s: %w", filepath.Join(d.path, name), err) |
| + // A holder's removal landed inside the open: start again. |
| + lockNamedPause(deadline) |
| + continue |
| } |
| lockNamedOpened(name) |
| if err := flockWithin(f, deadline); err != nil { |
| f.Close() |
| if errors.Is(err, syscall.EWOULDBLOCK) { |
| - err = busy |
| + return timedOut() |
| } |
| return fmt.Errorf("lock %s: %w", filepath.Join(d.path, name), err) |
| } |
| @@ -140,18 +150,25 @@ func (d *movesDir) lockNamed(name string, wait time.Duration, busy error) error |
| return nil |
| } |
| // The holder before us removed this file as it released it. Start |
| - // again only within the wait: the publication waits inside the |
| + // again, within the wait: the publication waits inside the |
| // registry's freeze, so a name that keeps changing ends in busy. |
| f.Close() |
| - if time.Now().After(deadline) { |
| - return fmt.Errorf("lock %s: %w", filepath.Join(d.path, name), busy) |
| - } |
| + lockNamedPause(deadline) |
| } |
| } |
| |
| -// flockWithin takes an exclusive flock on f, polling until deadline. Once |
| -// the deadline has passed it takes nothing and returns EWOULDBLOCK, so a |
| -// retry that reaches it late never succeeds past the wait. |
| +// lockNamedPause sleeps lockNamedRetryPause, or what is left of the wait |
| +// when that is less. |
| +func lockNamedPause(deadline time.Time) { |
| + if left := time.Until(deadline); left > 0 { |
| + time.Sleep(min(lockNamedRetryPause, left)) |
| + } |
| +} |
| + |
| +// flockWithin takes an exclusive flock on f, polling until deadline, and |
| +// once the deadline has passed stops trying and returns EWOULDBLOCK. A |
| +// flock that lands just after the deadline is kept: it is a valid, |
| +// exclusive lock, and the deadline only bounds the wait. |
| func flockWithin(f *os.File, deadline time.Time) error { |
| for { |
| if time.Now().After(deadline) { |
| @@ -229,6 +246,14 @@ func (d *movesDir) unlockRemoving() error { |
| return nil |
| } |
| |
| +// lockNamedRetryPause is the pause between lockNamed's retries after a |
| +// holder's removal, capped at what is left of the wait. |
| +const lockNamedRetryPause = 10 * time.Millisecond |
| + |
| +// lockNamedOpenAt opens the lock file for lockNamed. A test seam: a test |
| +// can make every open meet the race with a holder's removal. |
| +var lockNamedOpenAt = safefile.OpenFileAt |
| + |
| // lockNamedOpened runs in lockNamed after each open of the lock file and |
| // before its flock. A test seam: the window in which the file a waiter has |
| // open can be removed by the holder before it and replaced by a new one. |