| 1 | //go:build linux |
| 2 | |
| 3 | package host |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | "encoding/json" |
| 8 | "errors" |
| 9 | "fmt" |
| 10 | "os" |
| 11 | "testing" |
| 12 | |
| 13 | "github.com/carlosframework/platform/internal/hostclaim" |
| 14 | "github.com/carlosframework/platform/internal/hostguard" |
| 15 | "github.com/carlosframework/platform/internal/registry" |
| 16 | "github.com/carlosframework/platform/internal/release" |
| 17 | ) |
| 18 | |
| 19 | type plan9D4RetagConflict struct { |
| 20 | release.Store |
| 21 | armed, failed bool |
| 22 | } |
| 23 | |
| 24 | func (s *plan9D4RetagConflict) Put(ctx context.Context, key string, body []byte, etag string) (string, error) { |
| 25 | var c hostclaim.Claim |
| 26 | if s.armed && json.Unmarshal(body, &c) == nil && c.Holder != nil && c.Inventory["box-1"].Paths[recoveryHost+".db.gen"] == c.Holder.Stamp { |
| 27 | s.armed = false |
| 28 | s.failed = true |
| 29 | return "", release.ErrCASConflict |
| 30 | } |
| 31 | return s.Store.Put(ctx, key, body, etag) |
| 32 | } |
| 33 | |
| 34 | func TestPlan9D4RetirementCASFailureReplaysWithoutMarker(t *testing.T) { |
| 35 | d := plan9D4Landing(t) |
| 36 | st := &plan9D4RetagConflict{Store: d.f.claims} |
| 37 | d.f.claims = st |
| 38 | d.f.guard.Claims = hostguard.StoreReader{Store: st} |
| 39 | c := d.controller("boot-1", "controller-1", 4101, "start-1", true) |
| 40 | first := d.mustStart(t, c, plan9D4Probe, false) |
| 41 | st.armed = true |
| 42 | second, err := d.start(t, c, plan9D4Probe, true) |
| 43 | if !errors.Is(err, release.ErrCASConflict) || !st.failed { |
| 44 | t.Fatalf("retag CAS was not interrupted: %v", err) |
| 45 | } |
| 46 | if _, err := os.Stat(d.db + ".gen"); !errors.Is(err, os.ErrNotExist) { |
| 47 | t.Fatalf("marker still present after park: %v", err) |
| 48 | } |
| 49 | if got := d.f.read().Claim.Inventory["box-1"].Paths[recoveryHost+".db.gen"]; got != first.Claim.Holder.Stamp { |
| 50 | t.Fatalf("tag changed on failed CAS: %d", got) |
| 51 | } |
| 52 | // Replay the same admitted start. Its absent marker must not bypass the |
| 53 | // unfinished retirement or permit bytes under the predecessor's tag. |
| 54 | start, err := c.runtime.Prepare(t.Context(), registry.Route{Host: recoveryHost}, second, plan9D4Probe) |
| 55 | if err != nil { |
| 56 | t.Fatal(err) |
| 57 | } |
| 58 | if err := start.Restore(t.Context()); err != nil { |
| 59 | t.Fatal(err) |
| 60 | } |
| 61 | if err := os.WriteFile(d.db, []byte(fmt.Sprintf("written by session %d", second.Claim.Holder.Session)), 0600); err != nil { |
| 62 | t.Fatal(err) |
| 63 | } |
| 64 | d.requireRetired(t, first, second, false) |
| 65 | third := d.mustStart(t, c, plan9D4Probe, true) |
| 66 | d.requireRetired(t, second, third, false) |
| 67 | } |
| 68 | |
| 69 | func TestPlan9D4RetirementCollectorKeepsItsSelectedChore(t *testing.T) { |
| 70 | d := plan9D4Landing(t) |
| 71 | c := d.controller("boot-1", "controller-1", 4101, "start-1", true) |
| 72 | d.mustStart(t, c, plan9D4Probe, false) |
| 73 | d.step(t, "retirement chore", hostclaim.QueueLocalPark("m1", "box-1", func() string { return "retire-this-installation" })) |
| 74 | h := d.f.read().Claim.Holder |
| 75 | row := registry.Route{Host: recoveryHost, ClaimOwnership: registry.ClaimOwnership{ClaimIncarnation: h.Incarnation, ClaimStamp: h.Stamp}} |
| 76 | target, err := c.assembly.collectRetirementStop(t.Context(), row) |
| 77 | if err != nil { |
| 78 | t.Fatal(err) |
| 79 | } |
| 80 | if target.ObligationID != "retire-this-installation" || len(d.f.requests) != 1 || d.f.requests[0].ObligationID != target.ObligationID { |
| 81 | t.Fatalf("retirement relabelled as a wake stop: %+v", target) |
| 82 | } |
| 83 | } |
| 84 | |