//go:build linux package host import ( "context" "encoding/json" "errors" "fmt" "os" "testing" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" ) type plan9D4RetagConflict struct { release.Store armed, failed bool } func (s *plan9D4RetagConflict) Put(ctx context.Context, key string, body []byte, etag string) (string, error) { var c hostclaim.Claim if s.armed && json.Unmarshal(body, &c) == nil && c.Holder != nil && c.Inventory["box-1"].Paths[recoveryHost+".db.gen"] == c.Holder.Stamp { s.armed = false s.failed = true return "", release.ErrCASConflict } return s.Store.Put(ctx, key, body, etag) } func TestPlan9D4RetirementCASFailureReplaysWithoutMarker(t *testing.T) { d := plan9D4Landing(t) st := &plan9D4RetagConflict{Store: d.f.claims} d.f.claims = st d.f.guard.Claims = hostguard.StoreReader{Store: st} c := d.controller("boot-1", "controller-1", 4101, "start-1", true) first := d.mustStart(t, c, plan9D4Probe, false) st.armed = true second, err := d.start(t, c, plan9D4Probe, true) if !errors.Is(err, release.ErrCASConflict) || !st.failed { t.Fatalf("retag CAS was not interrupted: %v", err) } if _, err := os.Stat(d.db + ".gen"); !errors.Is(err, os.ErrNotExist) { t.Fatalf("marker still present after park: %v", err) } if got := d.f.read().Claim.Inventory["box-1"].Paths[recoveryHost+".db.gen"]; got != first.Claim.Holder.Stamp { t.Fatalf("tag changed on failed CAS: %d", got) } // Replay the same admitted start. Its absent marker must not bypass the // unfinished retirement or permit bytes under the predecessor's tag. start, err := c.runtime.Prepare(t.Context(), registry.Route{Host: recoveryHost}, second, plan9D4Probe) if err != nil { t.Fatal(err) } if err := start.Restore(t.Context()); err != nil { t.Fatal(err) } if err := os.WriteFile(d.db, []byte(fmt.Sprintf("written by session %d", second.Claim.Holder.Session)), 0600); err != nil { t.Fatal(err) } d.requireRetired(t, first, second, false) third := d.mustStart(t, c, plan9D4Probe, true) d.requireRetired(t, second, third, false) } func TestPlan9D4RetirementCollectorKeepsItsSelectedChore(t *testing.T) { d := plan9D4Landing(t) c := d.controller("boot-1", "controller-1", 4101, "start-1", true) d.mustStart(t, c, plan9D4Probe, false) d.step(t, "retirement chore", hostclaim.QueueLocalPark("m1", "box-1", func() string { return "retire-this-installation" })) h := d.f.read().Claim.Holder row := registry.Route{Host: recoveryHost, ClaimOwnership: registry.ClaimOwnership{ClaimIncarnation: h.Incarnation, ClaimStamp: h.Stamp}} target, err := c.assembly.collectRetirementStop(t.Context(), row) if err != nil { t.Fatal(err) } if target.ObligationID != "retire-this-installation" || len(d.f.requests) != 1 || d.f.requests[0].ObligationID != target.ObligationID { t.Fatalf("retirement relabelled as a wake stop: %+v", target) } }