package host import ( "bytes" "context" "errors" "os" "path/filepath" "testing" "time" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostguard" "golang.org/x/sys/unix" ) func TestR2LegacyLockContentionRetries(t *testing.T) { if !plan14RootFixture(t) { return } for _, scenario := range []string{"contention", "fifo", "foreign owner", "io error"} { t.Run(scenario, func(t *testing.T) { f := newLaunchFixture(t, servingOp()) g := f.root.Guard store := &rollbackDisk{dir: t.TempDir()} g.Root = hostguard.NewRootAuthority(store) in := hostguard.RootInstallation{Host: f.target.Host, Account: "bab", App: "app", Record: "record", Incarnation: f.target.Incarnation, Stamp: f.target.Stamp, Box: f.target.Box, Enrollment: f.target.Enrollment, Migration: 1} // The fixture's target carries installation identity; the claim remains // the authority checked by EstablishRoot and BeginFence. f.claims.set(func(c *hostclaim.Claim) { c.Holder.Account = in.Account c.Holder.App = in.App c.Holder.Admissions = []hostclaim.AdmissionRecord{{Box: f.target.Box, Region: f.target.Region, Session: f.target.Session, Generation: c.Holder.Generation}} }) if err := g.EstablishRoot(t.Context(), in, func(*hostclaim.Claim) error { return nil }); err != nil { t.Fatal(err) } before, err := store.Read(in.Host + ".json") if err != nil { t.Fatal(err) } path := filepath.Join(g.LockDir, in.Host+".lock") var held *os.File switch scenario { case "contention": held, err = os.OpenFile(path, os.O_RDWR, 0660) if err != nil { t.Fatal(err) } defer held.Close() if err := unix.Flock(int(held.Fd()), unix.LOCK_EX|unix.LOCK_NB); err != nil { t.Fatal(err) } defer unix.Flock(int(held.Fd()), unix.LOCK_UN) case "fifo": if err := os.Remove(path); err != nil { t.Fatal(err) } if err := unix.Mkfifo(path, 0660); err != nil { t.Fatal(err) } case "foreign owner": if err := unix.Mount("/foreign", path, "", unix.MS_BIND, ""); err != nil { t.Fatal(err) } defer unix.Unmount(path, 0) case "io error": g.LockDir = filepath.Join(g.LockDir, "missing") } target := f.target target.FenceID = "fix3-fence" start := time.Now() _, err = g.BeginFence(t.Context(), target) if err == nil { t.Fatal("mutation crossed unavailable lock") } if time.Since(start) > time.Second { t.Fatal("lock bound exceeded") } after, readErr := store.Read(in.Host + ".json") if readErr != nil { t.Fatal(readErr) } captured := hostguard.Captured{Incarnation: in.Incarnation, Stamp: in.Stamp} _, viewErr := g.Root.View(t.Context(), in.Host, captured) if scenario == "fifo" || scenario == "foreign owner" { if !errors.Is(err, hostguard.ErrUnsafeFile) || !errors.Is(viewErr, hostguard.ErrFenced) || bytes.Equal(before, after) { t.Fatal("hostile lock not durably quarantined", err, viewErr) } } else { if !bytes.Equal(before, after) || viewErr != nil { t.Fatal("transient lock failure changed durable state", err, viewErr) } } if scenario == "contention" { if !errors.Is(err, context.DeadlineExceeded) { t.Fatal(err) } if err := unix.Flock(int(held.Fd()), unix.LOCK_UN); err != nil { t.Fatal(err) } if _, err := g.BeginFence(t.Context(), target); err != nil { t.Fatal("released lock did not retry", err) } } }) } }