//go:build unix package hostbox import ( "context" "errors" "os" "path/filepath" "testing" "time" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/hostwrite" "github.com/carlosframework/platform/internal/release" ) func workFixture(t *testing.T) (WorkSource, release.Store, hostclaim.Observed) { t.Helper() ctx := context.Background() dir := t.TempDir() locks := filepath.Join(dir, "locks") for _, p := range []string{locks, filepath.Join(dir, "hostguard")} { if err := os.Mkdir(p, 0770); err != nil { t.Fatal(err) } } store := release.NewDirStore(t.TempDir()) obs, err := hostclaim.Read(ctx, store, "shop.example") if err != nil { t.Fatal(err) } obs, err = hostclaim.Update(ctx, store, obs, hostclaim.Adopt(hostclaim.NewHolder{Family: "instance", Key: "apps/acct/app/instances/shop.example.json", Account: "acct", App: "app", Host: "shop.example", Region: "ie"}, "box", nil, nil, "adopt", func() string { return "id" })) if err != nil { t.Fatal(err) } source := WorkSource{Guard: hostguard.Guard{DataDir: dir, LockDir: locks, Claims: hostguard.StoreReader{Store: store}}, Box: "box", Region: "ie", Now: func() time.Time { return time.Unix(100, 0) }} source.Policy = func(_ context.Context, c *hostclaim.Claim, s Selection, _ hostwrite.Identity) error { return Serving(c, s) } source.Local = func(context.Context, *hostclaim.Claim, Selection) error { return nil } if err := source.Guard.Activate(ctx, "shop.example", "box", nil); err != nil { t.Fatal(err) } return source, store, obs } func TestSelectedWorkRejectsChangesAndNeverRecaptures(t *testing.T) { for _, mode := range []string{"phase", "executor", "round-trip", "lease", "held"} { t.Run(mode, func(t *testing.T) { source, store, obs := workFixture(t) localLost, heldChanged := false, false source.Local = func(context.Context, *hostclaim.Claim, Selection) error { if localLost { return ErrUnsupported } return nil } source.Policy = func(_ context.Context, c *hostclaim.Claim, s Selection, i hostwrite.Identity) error { if heldChanged || i.Config != "selected" { return ErrChanged } return Serving(c, s) } ctx := context.Background() work, err := source.Select(ctx, "shop.example", "config") if err != nil { t.Fatal(err) } writes := 0 if err := work.With(ctx, hostwrite.Identity{Config: "selected"}, func() error { writes++; return nil }); err != nil { t.Fatal(err) } switch mode { case "phase": _, err = hostclaim.Update(ctx, store, obs, hostclaim.BeginDelete("delete", "test", "etag", time.Unix(1, 0), []string{"ie"}, []string{"box"}, "")) case "executor": _, err = hostclaim.Update(ctx, store, obs, func(c *hostclaim.Claim) error { c.Op.Exec = "other"; c.Op.Participants = []string{"other"}; return nil }) case "round-trip": _, err = hostclaim.Update(ctx, store, obs, hostclaim.AdmitWriter(hostclaim.Admission{Box: "box", Mode: "cold", NewGeneration: "next", Region: "ie", PredecessorRegion: "ie"})) if err == nil { err = source.Guard.Activate(ctx, "shop.example", "box", nil) } case "lease": localLost = true case "held": heldChanged = true } if err != nil { t.Fatal(err) } if err := work.With(ctx, hostwrite.Identity{Config: "selected"}, func() error { writes++; return nil }); err == nil { t.Fatal("stale write admitted") } if writes != 1 { t.Fatal(writes) } }) } } func TestSelectedWorkNeedsLocalRecordAndProviders(t *testing.T) { source, _, _ := workFixture(t) source.Local = nil if _, err := source.Select(context.Background(), "shop.example", "env"); !errors.Is(err, ErrUnsupported) { t.Fatal(err) } source.Local = func(context.Context, *hostclaim.Claim, Selection) error { return nil } if err := os.Remove(filepath.Join(source.Guard.DataDir, "hostguard", "shop.example.json")); err != nil { t.Fatal(err) } if _, err := source.Select(context.Background(), "shop.example", "env"); !errors.Is(err, hostguard.ErrNoRecord) { t.Fatal(err) } } // Every execution action waits out quiet_after[region], a relocation // target's probe spawn among them (ยง5.8: like a push). func TestSelectedWorkExecutionWaitsOutTheQuietWindow(t *testing.T) { source, store, obs := workFixture(t) source.Policy = func(context.Context, *hostclaim.Claim, Selection, hostwrite.Identity) error { return nil } ctx := context.Background() quiet := time.Unix(200, 0) if _, err := hostclaim.Update(ctx, store, obs, func(c *hostclaim.Claim) error { c.QuietAfter = map[string]time.Time{"ie": quiet} return nil }); err != nil { t.Fatal(err) } run := func(now time.Time, action string) error { at := source at.Now = func() time.Time { return now } work, err := at.Select(ctx, "shop.example", action) if err != nil { return err } return work.With(ctx, hostwrite.Identity{}, func() error { return nil }) } for _, action := range []string{"probe", "spawn", "install", "restore", "seed", "writer"} { if err := run(quiet, action); !errors.Is(err, ErrChanged) { t.Fatalf("%s inside the quiet window: %v", action, err) } if err := run(quiet.Add(time.Second), action); err != nil { t.Fatalf("%s after the quiet window: %v", action, err) } } if err := run(quiet, "config"); err != nil { t.Fatalf("a non-execution action waited: %v", err) } } // A relocation probe is an execution action: the region's quiet-after // window refuses it like spawn (claim Plan 11). func TestPlan11QuietAfterRefusesTheProbeAction(t *testing.T) { source, store, obs := workFixture(t) ctx := context.Background() work, err := source.Select(ctx, "shop.example", "probe") if err != nil { t.Fatal(err) } if _, err := hostclaim.Update(ctx, store, obs, func(c *hostclaim.Claim) error { c.QuietAfter = map[string]time.Time{"ie": time.Unix(200, 0)} return nil }); err != nil { t.Fatal(err) } ran := false if err := work.With(ctx, hostwrite.Identity{}, func() error { ran = true; return nil }); !errors.Is(err, ErrChanged) || ran { t.Fatalf("probe ran inside quiet-after: %v %v", err, ran) } }