feat(v0.4): add WithLock helper that warns-and-skips on timeout
This commit is contained in:
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user