Confine cleanup and use held session locks
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
package artifacts
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestEnsureLayoutCreatesExpectedDirectories(t *testing.T) {
|
||||
@@ -93,8 +96,148 @@ func TestLockAcquireRelease(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Exists() error = %v", err)
|
||||
}
|
||||
if exists {
|
||||
t.Fatalf("expected lock file %q to be removed", lock.path)
|
||||
if !exists {
|
||||
t.Fatalf("expected lock metadata file %q to remain", lock.path)
|
||||
}
|
||||
secondLock, err := store.AcquireSessionLockFor("sample-campaign", "session-1")
|
||||
if err != nil {
|
||||
t.Fatalf("AcquireSessionLockFor() after release error = %v", err)
|
||||
}
|
||||
if err := store.ReleaseSessionLock(secondLock); err != nil {
|
||||
t.Fatalf("ReleaseSessionLock(second lock) error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLockWaitHonorsCancellation(t *testing.T) {
|
||||
store := NewLocalStore(t.TempDir())
|
||||
lock, err := store.AcquireSessionLockFor("sample-campaign", "session-1")
|
||||
if err != nil {
|
||||
t.Fatalf("AcquireSessionLockFor() error = %v", err)
|
||||
}
|
||||
defer func() { _ = store.ReleaseSessionLock(lock) }()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
_, err = store.AcquireSessionLockForContext(ctx, "sample-campaign", "session-1")
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("AcquireSessionLockForContext() error = %v, want context cancellation", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLockWaitsUntilRelease(t *testing.T) {
|
||||
store := NewLocalStore(t.TempDir())
|
||||
first, err := store.AcquireSessionLockFor("sample-campaign", "session-1")
|
||||
if err != nil {
|
||||
t.Fatalf("AcquireSessionLockFor() error = %v", err)
|
||||
}
|
||||
acquired := make(chan *LockHandle, 1)
|
||||
errs := make(chan error, 1)
|
||||
go func() {
|
||||
lock, err := store.AcquireSessionLockForContext(context.Background(), "sample-campaign", "session-1")
|
||||
if err != nil {
|
||||
errs <- err
|
||||
return
|
||||
}
|
||||
acquired <- lock
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-errs:
|
||||
t.Fatalf("AcquireSessionLockForContext() error = %v", err)
|
||||
case lock := <-acquired:
|
||||
_ = store.ReleaseSessionLock(lock)
|
||||
t.Fatal("AcquireSessionLockForContext() acquired before the held lock was released")
|
||||
case <-time.After(150 * time.Millisecond):
|
||||
}
|
||||
if err := store.ReleaseSessionLock(first); err != nil {
|
||||
t.Fatalf("ReleaseSessionLock(first) error = %v", err)
|
||||
}
|
||||
select {
|
||||
case err := <-errs:
|
||||
t.Fatalf("AcquireSessionLockForContext() error = %v", err)
|
||||
case lock := <-acquired:
|
||||
if err := store.ReleaseSessionLock(lock); err != nil {
|
||||
t.Fatalf("ReleaseSessionLock(waiting lock) error = %v", err)
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("AcquireSessionLockForContext() did not acquire after release")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLockRecoversAfterLockHolderDies(t *testing.T) {
|
||||
if os.Getenv("NARRATIO_LOCK_PROCESS_HELPER") == "1" {
|
||||
store := NewLocalStore(os.Getenv("NARRATIO_LOCK_PROCESS_ROOT"))
|
||||
lock, err := store.AcquireSessionLockFor("sample-campaign", "session-1")
|
||||
if err != nil {
|
||||
os.Exit(2)
|
||||
}
|
||||
if err := os.WriteFile(os.Getenv("NARRATIO_LOCK_PROCESS_READY"), []byte(lock.path), 0o600); err != nil {
|
||||
os.Exit(3)
|
||||
}
|
||||
select {}
|
||||
}
|
||||
|
||||
root := t.TempDir()
|
||||
ready := filepath.Join(t.TempDir(), "ready")
|
||||
cmd := exec.Command(os.Args[0], "-test.run=^TestLockRecoversAfterLockHolderDies$")
|
||||
cmd.Env = append(os.Environ(),
|
||||
"NARRATIO_LOCK_PROCESS_HELPER=1",
|
||||
"NARRATIO_LOCK_PROCESS_ROOT="+root,
|
||||
"NARRATIO_LOCK_PROCESS_READY="+ready,
|
||||
)
|
||||
if err := cmd.Start(); err != nil {
|
||||
t.Fatalf("start lock-holder process: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if cmd.Process != nil {
|
||||
_ = cmd.Process.Kill()
|
||||
}
|
||||
_ = cmd.Wait()
|
||||
}()
|
||||
deadline := time.Now().Add(2 * time.Second)
|
||||
for {
|
||||
if _, err := os.Stat(ready); err == nil {
|
||||
break
|
||||
}
|
||||
if time.Now().After(deadline) {
|
||||
t.Fatal("lock-holder process did not signal readiness")
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
store := NewLocalStore(root)
|
||||
if _, err := store.AcquireSessionLockFor("sample-campaign", "session-1"); !errors.Is(err, ErrLockConflict) {
|
||||
t.Fatalf("AcquireSessionLockFor() error = %v, want lock conflict", err)
|
||||
}
|
||||
if err := cmd.Process.Kill(); err != nil {
|
||||
t.Fatalf("kill lock-holder process: %v", err)
|
||||
}
|
||||
if err := cmd.Wait(); err == nil {
|
||||
t.Fatal("lock-holder process exited without being killed")
|
||||
}
|
||||
cmd.Process = nil
|
||||
|
||||
lock, err := store.AcquireSessionLockFor("sample-campaign", "session-1")
|
||||
if err != nil {
|
||||
t.Fatalf("AcquireSessionLockFor() after holder death error = %v", err)
|
||||
}
|
||||
if err := store.ReleaseSessionLock(lock); err != nil {
|
||||
t.Fatalf("ReleaseSessionLock() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseSessionLockReportsUnlockFailure(t *testing.T) {
|
||||
store := NewLocalStore(t.TempDir())
|
||||
lock, err := store.AcquireSessionLockFor("sample-campaign", "session-1")
|
||||
if err != nil {
|
||||
t.Fatalf("AcquireSessionLockFor() error = %v", err)
|
||||
}
|
||||
|
||||
original := releaseHeldFileLockFn
|
||||
releaseHeldFileLockFn = func(*os.File) error { return errors.New("unlock failed") }
|
||||
t.Cleanup(func() { releaseHeldFileLockFn = original })
|
||||
if err := store.ReleaseSessionLock(lock); err == nil || !strings.Contains(err.Error(), "unlock failed") {
|
||||
t.Fatalf("ReleaseSessionLock() error = %v, want unlock failure", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user