feat(v0.4): add WithLock helper that warns-and-skips on timeout

This commit is contained in:
2026-04-21 17:01:36 -04:00
parent dc16713f11
commit 1c62410a06
2 changed files with 66 additions and 0 deletions
+17
View File
@@ -52,3 +52,20 @@ func Acquire(lockPath string, timeout, staleAfter time.Duration) (func(), error)
}
}
}
// WithLock acquires the lock at lockPath, runs fn while holding it, and
// releases the lock. If the lock cannot be acquired within timeout, fn is
// NOT called and (skipped=true, err=nil) is returned — this matches the
// ctask "warn and skip, never hang" contract for metadata writes.
// Any error returned by fn is surfaced to the caller with skipped=false.
func WithLock(lockPath string, timeout, staleAfter time.Duration, fn func() error) (skipped bool, err error) {
release, err := Acquire(lockPath, timeout, staleAfter)
if err != nil {
if errors.Is(err, ErrTimeout) {
return true, nil
}
return false, err
}
defer release()
return false, fn()
}
+49
View File
@@ -100,3 +100,52 @@ func TestStaleLockIsRemoved(t *testing.T) {
}
defer release()
}
func TestWithLockRunsFunction(t *testing.T) {
dir := t.TempDir()
lockPath := filepath.Join(dir, "write.lock")
called := false
skipped, err := WithLock(lockPath, 1*time.Second, 10*time.Second, func() error {
called = true
return nil
})
if err != nil {
t.Fatalf("WithLock: %v", err)
}
if skipped {
t.Error("should not have skipped on successful acquire")
}
if !called {
t.Error("fn was not called")
}
if _, err := os.Stat(lockPath); !os.IsNotExist(err) {
t.Errorf("lock should be released after WithLock, got err=%v", err)
}
}
func TestWithLockSkipsOnTimeout(t *testing.T) {
dir := t.TempDir()
lockPath := filepath.Join(dir, "write.lock")
r1, err := Acquire(lockPath, 1*time.Second, 10*time.Second)
if err != nil {
t.Fatalf("first Acquire: %v", err)
}
defer r1()
called := false
skipped, err := WithLock(lockPath, 200*time.Millisecond, 10*time.Second, func() error {
called = true
return nil
})
if err != nil {
t.Fatalf("WithLock should not return error on timeout: %v", err)
}
if !skipped {
t.Error("expected skipped=true on timeout")
}
if called {
t.Error("fn should not have been called on timeout")
}
}