summaryrefslogtreecommitdiff
path: root/internal/filelock/filelock.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/filelock/filelock.go')
-rw-r--r--internal/filelock/filelock.go13
1 files changed, 9 insertions, 4 deletions
diff --git a/internal/filelock/filelock.go b/internal/filelock/filelock.go
index 192c3ca..bc3ae8a 100644
--- a/internal/filelock/filelock.go
+++ b/internal/filelock/filelock.go
@@ -21,22 +21,27 @@ func UnlockExclusive(f *os.File) error {
return unlockExclusive(f.Fd())
}
+// retryInterval is the backoff between successive non-blocking lock attempts.
+const retryInterval = 5 * time.Millisecond
+
// AcquireExclusive spins with TryExclusive until the lock is acquired, ctx is done, or a non-would-block error occurs.
func AcquireExclusive(ctx context.Context, f *os.File) (func() error, error) {
fd := f.Fd()
- retryTimer := time.NewTimer(5 * time.Millisecond)
- defer retryTimer.Stop()
for {
err := tryLockExclusive(fd)
if err == nil {
return func() error { return unlockExclusive(fd) }, nil
}
if errors.Is(err, ErrWouldBlock) {
- retryTimer.Reset(5 * time.Millisecond)
+ // Use a fresh timer per iteration via time.After instead of reusing and
+ // Reset()-ing a single timer. Resetting a timer that may still be pending is
+ // the documented hazard in the Go timer API: a stale value can already be
+ // queued on the channel, causing a spurious early wake-up. A new timer each
+ // loop guarantees a clean, full retryInterval delay (or ctx cancellation).
select {
case <-ctx.Done():
return nil, ctx.Err()
- case <-retryTimer.C:
+ case <-time.After(retryInterval):
}
continue
}