| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "strings" |
| 6 | "testing" |
| 7 | "time" |
| 8 | |
| 9 | "github.com/carlosframework/platform/internal/hostclaim" |
| 10 | "github.com/carlosframework/platform/internal/lease" |
| 11 | "github.com/carlosframework/platform/internal/moves" |
| 12 | "github.com/carlosframework/platform/internal/park" |
| 13 | "github.com/carlosframework/platform/internal/provision" |
| 14 | ) |
| 15 | |
| 16 | // Ruling R-PARK (settings-4 Task 8b): <holder.Location>/build-state.json is |
| 17 | // live state beside the lease, the generation marker's twin. Every path |
| 18 | // that lists the holder location passes it over where it passes over the |
| 19 | // marker; anywhere else it is residue like any other object. |
| 20 | |
| 21 | // A landing onto a location that already holds a build state (an earlier |
| 22 | // stay's) parks the earlier stay's generations, passes the build state over |
| 23 | // as it passes the marker over, and completes: the install then places the |
| 24 | // moved state over it. |
| 25 | func TestALandingOntoALocationHoldingABuildStateCompletes(t *testing.T) { |
| 26 | t.Parallel() |
| 27 | h := newLandingHarness(t) |
| 28 | storeState(h.phase1Harness, "data-ie", cutRoot+"/build-state.json", "v6") |
| 29 | h.flip(t) |
| 30 | keys := h.residue(t) |
| 31 | dst := cutRoot + "/build-state.json" |
| 32 | if _, err := h.vs.PutIfAbsent(context.Background(), "data-uk", dst, []byte(p1StateBody("v3"))); err != nil { |
| 33 | t.Fatal(err) |
| 34 | } |
| 35 | for i := 0; i < 4 && h.read(t, p1Host).Move.LandingPhase != provision.LandRestored; i++ { |
| 36 | h.land(t) |
| 37 | } |
| 38 | h.installed(t) |
| 39 | if got := h.f.buckets["data-uk"][dst].body; got != p1StateBody("v6") { |
| 40 | t.Fatalf("the location holds %q, want the moved v6", got) |
| 41 | } |
| 42 | for _, k := range keys { |
| 43 | if _, ok := h.f.buckets["data-uk"][k]; ok && k != cutRoot+"/generation.json" { |
| 44 | t.Fatalf("the earlier stay's %s was not parked", k) |
| 45 | } |
| 46 | } |
| 47 | for k := range h.f.buckets["data-uk"] { |
| 48 | if strings.HasPrefix(k, "parked/") && strings.Contains(k, "/build-state.json@") { |
| 49 | t.Fatalf("the location's build state was parked: %s", k) |
| 50 | } |
| 51 | } |
| 52 | } |
| 53 | |
| 54 | // A dirty return whose source location holds the pre-move build state |
| 55 | // classifies the location, parks the pre-move generations and installs; |
| 56 | // the returned state is placed over the pre-move one. |
| 57 | func TestADirtyReturnOverAPreMoveBuildStateClassifies(t *testing.T) { |
| 58 | t.Parallel() |
| 59 | h := newReturnHarness(t, true) |
| 60 | ctx := context.Background() |
| 61 | if _, err := h.vs.PutIfAbsent(ctx, "data-ie", cutRoot+"/build-state.json", []byte(p1StateBody("v5"))); err != nil { |
| 62 | t.Fatal(err) |
| 63 | } |
| 64 | key := moves.BuildStateKey(p1Host, p1RB) |
| 65 | ver, err := h.vs.PutIfAbsent(ctx, "data-ie", key, []byte(p1StateBody("v8"))) |
| 66 | if err != nil { |
| 67 | t.Fatal(err) |
| 68 | } |
| 69 | h.seal.State = []moves.SealedObject{{Key: key, VersionID: ver, Size: int64(len(p1StateBody("v8")))}} |
| 70 | if err := h.seal.Validate(); err != nil { |
| 71 | t.Fatal(err) |
| 72 | } |
| 73 | pinned := h.read(t, p1Host).Move.Inputs |
| 74 | if refused, err := h.install(t); err != nil || refused != "" { |
| 75 | t.Fatalf("install: %q %v", refused, err) |
| 76 | } |
| 77 | h.installedReturn(t, pinned) |
| 78 | if got := h.f.buckets["data-ie"][cutRoot+"/build-state.json"].body; got != p1StateBody("v8") { |
| 79 | t.Fatalf("the source's location holds %q, want the returned v8", got) |
| 80 | } |
| 81 | } |
| 82 | |
| 83 | // TestMovesHolderBuildStateMatchesTheEngine keeps the host's twin key in |
| 84 | // step with the park engine's, in the spirit of |
| 85 | // TestMovesParkLockMatchesTheEngine: park.Decide is the oracle. In the |
| 86 | // authoritative region, with a classifier that places every key (so a |
| 87 | // skip is the engine's alone), Decide makes no candidate of a one-key |
| 88 | // census exactly when the host says the key is the holder location's |
| 89 | // build state. The same file name anywhere else is a candidate. |
| 90 | func TestMovesHolderBuildStateMatchesTheEngine(t *testing.T) { |
| 91 | for name, bare := range map[string]bool{"scoped": false, "bare": true} { |
| 92 | t.Run(name, func(t *testing.T) { |
| 93 | f := newEvFixture(t) |
| 94 | if bare { |
| 95 | f.bare(t) |
| 96 | } |
| 97 | obs := f.observe(t) |
| 98 | view := park.View{ClaimETag: obs.ETag, Region: "ie", AuthoritativeRegion: "ie", MarkerKnown: true, MarkerGeneration: evMoveID, |
| 99 | Marker: park.Ref{Key: f.location + "/generation.json"}, |
| 100 | GenerationOf: func(r park.Ref) (string, bool) { |
| 101 | if g, ok := landingGenerationOf(f.location, r.Key); ok { |
| 102 | return g, true |
| 103 | } |
| 104 | return "stale", true |
| 105 | }} |
| 106 | other := strings.Repeat("ab", 32) |
| 107 | keys := []string{ |
| 108 | f.location + "/build-state.json", |
| 109 | f.location + "/g-old/build-state.json", |
| 110 | f.location + "/build-state.json.old", |
| 111 | f.location + "/state/build-state.json", |
| 112 | "instances/" + evHost + "/" + other + "/build-state.json", |
| 113 | "moves/" + evHost + "/build-state.json", |
| 114 | "moves/" + evHost + "/" + evMoveID + "/state/build-state.json", |
| 115 | } |
| 116 | if bare { |
| 117 | keys = append(keys, "instances/"+evHost+"/"+f.inc+"/build-state.json") |
| 118 | } else { |
| 119 | keys = append(keys, "instances/"+evHost+"/build-state.json") |
| 120 | } |
| 121 | skipped := 0 |
| 122 | for _, key := range keys { |
| 123 | d, err := park.Decide(obs, view, []lease.ObjectVersion{{Key: key, VersionID: "v1"}, {Key: key, VersionID: "v0"}}, f.inc) |
| 124 | if err != nil { |
| 125 | t.Fatalf("%s: %v", key, err) |
| 126 | } |
| 127 | engine, mirror := len(d.Candidates) == 0, movesHolderBuildState(f.location, key) |
| 128 | if engine != mirror { |
| 129 | t.Errorf("%s: the engine skips it %v, the host's twin key says %v", key, engine, mirror) |
| 130 | } |
| 131 | if engine { |
| 132 | skipped++ |
| 133 | } |
| 134 | } |
| 135 | if skipped != 1 { |
| 136 | t.Errorf("the engine skipped %d keys, want the holder location's build state alone", skipped) |
| 137 | } |
| 138 | }) |
| 139 | } |
| 140 | } |
| 141 | |
| 142 | // The close need not vouch for the live location's build state: it is the |
| 143 | // location's own, as the marker is. A lane's copy written after the close |
| 144 | // still has an unknown writer. |
| 145 | func TestTheLiveLocationsBuildStateNeedsNoVouching(t *testing.T) { |
| 146 | for name, bare := range map[string]bool{"scoped": false, "bare": true} { |
| 147 | t.Run(name, func(t *testing.T) { |
| 148 | f := newEvFixture(t) |
| 149 | if bare { |
| 150 | f.bare(t) |
| 151 | } |
| 152 | f.lane(evMoveID, "move.json") |
| 153 | f.bucket.Put("data", f.genKey(evMoveID), []byte("live")) |
| 154 | f.bucket.Put("data", f.location+"/generation.json", []byte("marker")) |
| 155 | f.bucket.Put("data", f.location+"/build-state.json", []byte("older")) |
| 156 | f.bucket.Put("data", f.location+"/build-state.json", []byte("current")) |
| 157 | if _, _, err := f.provider().VerifyRetirement(t.Context(), f.observe(t), f.census(t)); err != nil { |
| 158 | t.Fatalf("VerifyRetirement refused the live build state: %v", err) |
| 159 | } |
| 160 | }) |
| 161 | } |
| 162 | t.Run("a lane's state written after the close", func(t *testing.T) { |
| 163 | f := newEvFixture(t) |
| 164 | f.rec.LastMove.At = time.Date(2026, 10, 4, 0, 0, 0, 0, time.UTC) // before every write |
| 165 | f.lane("mv_unknown", moves.BuildStateSuffix) |
| 166 | bothRefuse(t, f, true) |
| 167 | }) |
| 168 | } |
| 169 | |
| 170 | // returnBeforeInstall is a rollback harness whose landing stopped at |
| 171 | // building, before the install placed anything, with the forward seal's |
| 172 | // state edited by edit. |
| 173 | func returnBeforeInstall(t *testing.T, edit func(h *rollbackHarness, s *moves.SealedObject)) *rollbackHarness { |
| 174 | t.Helper() |
| 175 | h := &rollbackHarness{landingHarness: newLandingHarness(t)} |
| 176 | h.f.keepHistory = true |
| 177 | storeState(h.phase1Harness, "data-ie", cutRoot+"/build-state.json", "v6") |
| 178 | h.flip(t) |
| 179 | h.landAt = "after-record-" + provision.LandBuilding |
| 180 | h.crashLand(t) |
| 181 | h.uncover(t) |
| 182 | rewriteP1Claim(t, h.s, func(c *hostclaim.Claim) { |
| 183 | s, err := journalledSeal(c) |
| 184 | if err != nil { |
| 185 | t.Fatal(err) |
| 186 | } |
| 187 | if len(s.State) != 1 { |
| 188 | t.Fatalf("seal state %+v", s.State) |
| 189 | } |
| 190 | edit(h, &s.State[0]) |
| 191 | c.Op.Journal[hostclaim.JournalSeal] = s.Encode() |
| 192 | }) |
| 193 | h.rollBack(t) |
| 194 | h.rollbackPass(t) |
| 195 | return h |
| 196 | } |
| 197 | |
| 198 | // Task 8 re-review minor 1: a return before the install reads the forward |
| 199 | // seal's state only at the move's own lane key, and only at the size the |
| 200 | // seal names, as copyAsGiven reads every other sealed object. |
| 201 | func TestAReturnBeforeTheInstallReadsOnlyItsOwnSealedState(t *testing.T) { |
| 202 | t.Parallel() |
| 203 | refused := func(t *testing.T, h *rollbackHarness) { |
| 204 | t.Helper() |
| 205 | rb := h.rb(t) |
| 206 | if rb.TargetPhase == provision.RbTargetReturned { |
| 207 | t.Fatalf("the return sealed: %+v", rb) |
| 208 | } |
| 209 | if _, ok := h.f.buckets["data-ie"][moves.BuildStateKey(p1Host, rb.ID)]; ok { |
| 210 | t.Fatal("a build state went back") |
| 211 | } |
| 212 | } |
| 213 | t.Run("the control returns", func(t *testing.T) { |
| 214 | h := returnBeforeInstall(t, func(*rollbackHarness, *moves.SealedObject) {}) |
| 215 | if rb := h.rb(t); rb.TargetPhase != provision.RbTargetReturned { |
| 216 | t.Fatalf("rollback %+v", rb) |
| 217 | } |
| 218 | }) |
| 219 | t.Run("another lane's key", func(t *testing.T) { |
| 220 | h := returnBeforeInstall(t, func(h *rollbackHarness, s *moves.SealedObject) { |
| 221 | key := moves.BuildStateKey(p1Host, "mv_other") |
| 222 | body := p1StateBody("v4") |
| 223 | ver, err := h.vs.PutIfAbsent(context.Background(), "data-uk", key, []byte(body)) |
| 224 | if err != nil { |
| 225 | t.Fatal(err) |
| 226 | } |
| 227 | s.Key, s.VersionID, s.Size = key, ver, int64(len(body)) |
| 228 | }) |
| 229 | refused(t, h) |
| 230 | }) |
| 231 | t.Run("a size the version is not", func(t *testing.T) { |
| 232 | h := returnBeforeInstall(t, func(_ *rollbackHarness, s *moves.SealedObject) { s.Size++ }) |
| 233 | refused(t, h) |
| 234 | }) |
| 235 | } |
| 236 | |
| 237 | // Task 8 re-review minor 2: once the landing reached probing a writer may |
| 238 | // have recorded over the placed state, so the target's location is the |
| 239 | // source of the returned state even if the landing's journal lost its |
| 240 | // install step (installManifest rebuilds it then; the build state is not |
| 241 | // rebuilt). |
| 242 | func TestAReturnPastProbingTakesTheTargetsStateWithoutTheInstallStep(t *testing.T) { |
| 243 | t.Parallel() |
| 244 | h := &rollbackHarness{landingHarness: newLandingHarness(t)} |
| 245 | storeState(h.phase1Harness, "data-ie", cutRoot+"/build-state.json", "v6") |
| 246 | h.flip(t) |
| 247 | h.land(t) |
| 248 | h.installed(t) |
| 249 | h.step(t, provision.LandRestored, provision.LandProbing) |
| 250 | h.tpos = h.n |
| 251 | storeState(h.phase1Harness, "data-uk", cutRoot+"/build-state.json", "v8") |
| 252 | j := h.landJournal(t) |
| 253 | j.Unmark(landStepInstall) |
| 254 | if err := j.Commit(h.tdir); err != nil { |
| 255 | t.Fatal(err) |
| 256 | } |
| 257 | h.uncover(t) |
| 258 | h.rollBack(t) |
| 259 | h.rollbackPass(t) |
| 260 | rb := h.rb(t) |
| 261 | if !rb.Dirty || rb.TargetPhase != provision.RbTargetReturned { |
| 262 | t.Fatalf("rollback %+v", rb) |
| 263 | } |
| 264 | if _, body := returnSealState(t, h, rb.ID); body != p1StateBody("v8") { |
| 265 | t.Fatalf("the return lane holds %q, want the target's v8", body) |
| 266 | } |
| 267 | } |
| 268 | |