carlos / platform Public

enginesim: staged classification by app; --revoke runs the barrier (#335 plan 1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev c9c3f4e5832ed7e0840bef36ad1c18b203315b2b parent 8c7d684
3 files changed, +162 −11
  • internal/sim/engines/classify_test.go +29 −9
  • internal/sim/engines/launch_test.go +37 −2
  • internal/sim/engines/witness_test.go +96 −0
diff --git a/internal/sim/engines/classify_test.go b/internal/sim/engines/classify_test.go
index 50ac8a968..e0032cb56 100644
--- a/internal/sim/engines/classify_test.go
+++ b/internal/sim/engines/classify_test.go
@@ -221,24 +221,44 @@ func (w *world) grant(b *box, host string) bool {
return true
}
-// revoke is --revoke doc-engine: the grant goes; the mark stays (becoming
-// public is never automatic).
-func (w *world) revoke(b *box, host string) {
+// revoke is --revoke doc-engine on any row of a group: the grant goes and
+// the group's mark stays (becoming public is never automatic; only --public
+// writes 0). Like --grant it runs the barrier for the group: it returns once
+// the edge has loaded the change, so the group's mark is fenced, or says it
+// timed out. Refused on a minted row whose owner cannot be resolved.
+func (w *world) revoke(b *box, host string) bool {
r := b.Rows[host]
if r == nil || !r.Grant {
- return
+ return false
+ }
+ if followsOwner(r) {
+ if _, ok := b.resolveOwner(r); !ok {
+ w.logf("box%d: --revoke doc-engine on %s: refused: the row's owner cannot be resolved", b.ID, host)
+ return false
+ }
}
+ g := w.group(b, host)
b.Wrote++
r.Grant = false
- w.logf("box%d: registry change %d: --revoke doc-engine on %s; the mark stays", b.ID, b.Wrote, host)
+ want := b.Wrote
+ w.logf("box%d: registry change %d: --revoke doc-engine on %s (group %s); the mark stays %v", b.ID, b.Wrote, host, g, w.groupMark(b, g))
+ for i := 0; i < fenceWaitTicks && b.Fenced < want; i++ {
+ w.reload(b)
+ }
+ if b.Fenced < want {
+ w.logf("box%d: --revoke on %s: the edge has not loaded it; timed out", b.ID, host)
+ return false
+ }
+ return true
}
// ---------- classification before serving ----------
// classify is the upgrade's classification of group g: 1 if a row of the
// group holds the grant, the live manifest declares, or the manifest of any
-// staged version declares (classify-staged); 0 if the live manifest is
-// readable and none of those holds; else NULL.
+// other version of the app staged on the box declares, whichever instance of
+// the app staged it (classify-staged); 0 if the live manifest is readable and
+// none of those holds; else NULL.
func (w *world) classify(b *box, g string) mark {
if w.groupHasGrant(b, g) {
return markInternal
@@ -249,7 +269,7 @@ func (w *world) classify(b *box, g string) mark {
return markInternal
}
if on("classify-staged") {
- for _, v := range b.Staged[g] {
+ for _, v := range w.stagedForApp(b, g) {
if bd := w.builds[v]; bd.ManifestOK && bd.Declares {
return markInternal
}
@@ -332,7 +352,7 @@ func (w *world) residualAtUpgrade(b *box) string {
continue
}
staged := false
- for _, v := range b.Staged[g] {
+ for _, v := range w.stagedForApp(b, g) {
if bd := w.builds[v]; bd.ManifestOK && bd.Declares {
staged = true
}
diff --git a/internal/sim/engines/launch_test.go b/internal/sim/engines/launch_test.go
index 02b2c2f8b..1ad5ce395 100644
--- a/internal/sim/engines/launch_test.go
+++ b/internal/sim/engines/launch_test.go
@@ -172,9 +172,12 @@ func (w *world) repoint(b *box, g string, ver int) bool {
return true
}
-// stage records that build ver of group g is staged on the box.
+// stage records that build ver of group g is staged on the box. A version is
+// staged once per app (/opt/carlos/versions holds the app's versions): if
+// another instance of the app already staged it, it stays recorded there
+// only, and this group runs it from that copy.
func (w *world) stage(b *box, g string, ver int) {
- for _, v := range b.Staged[g] {
+ for _, v := range w.stagedForApp(b, g) {
if v == ver {
return
}
@@ -182,6 +185,38 @@ func (w *world) stage(b *box, g string, ver int) {
b.Staged[g] = append(b.Staged[g], ver)
}
+// appGroups is group g and every other instance group on the box whose own
+// row has the same non-empty account/app, sorted.
+func (w *world) appGroups(b *box, g string) []string {
+ own := b.Rows[g]
+ if own == nil || own.App == "" {
+ return []string{g}
+ }
+ out := []string{}
+ seen := false
+ for _, h := range w.groups(b) {
+ if r := b.Rows[h]; r != nil && r.App == own.App {
+ out = append(out, h)
+ seen = seen || h == g
+ }
+ }
+ if !seen {
+ out = append(out, g)
+ sort.Strings(out)
+ }
+ return out
+}
+
+// stagedForApp is every version of g's app staged on the box, whichever
+// instance of the app staged it.
+func (w *world) stagedForApp(b *box, g string) []int {
+ var out []int
+ for _, h := range w.appGroups(b, g) {
+ out = append(out, b.Staged[h]...)
+ }
+ return out
+}
+
// wake is the activator waking group g by its wake plan: "recovery" starts
// the recovery build, "fallback" first fails to start the live build and
// falls back; both choose GoodVersion. The triggering request, if any, is
diff --git a/internal/sim/engines/witness_test.go b/internal/sim/engines/witness_test.go
index 5eace5b84..a7e70bf89 100644
--- a/internal/sim/engines/witness_test.go
+++ b/internal/sim/engines/witness_test.go
@@ -67,6 +67,8 @@ var witnesses = []witness{
"R2-2: the engine runs a declaring build whose live manifest cannot be read; the upgrade leaves it NULL; an edge that serves NULL like 0 admits a public request into the engine", wClassifyNullRefused},
{"classify-staged", "classify-staged", "INV-I1+INV-P",
"R8-1: an awake engine is repointed to a plain build without a restart, so the declaring build still runs; an upgrade that reads only the live manifest classifies the group 0, under the running engine (INV-I1) and a public request reaches it (INV-P)", wClassifyStaged},
+ {"classify-staged", "classify-staged", "INV-I1+INV-P",
+ "R8-1, app scope: two instances of one app; the declaring build is staged once, by the other instance; this one runs it from that copy, is repointed to a plain build without a restart and upgraded; read per group, or without the staged clause, it classifies 0 under the running engine", wClassifyStagedApp},
{"minted-follow-owner", "minted-follow-owner", "INV-P",
"R5-1: an app minted a platform row while public; adoption of a declaring build marks and fences the app; a minted row that is its own group stays 0 and a public request by the minted host reaches the engine", wMintedFollowOwner},
{"grant-marks-group", "grant-marks-group", "INV-P",
@@ -586,6 +588,34 @@ func wClassifyStaged(t *testing.T, w *world) {
w.check()
}
+// wClassifyStagedApp (R8-1, app scope): tcp-a stages the declaring build
+// first; tcp-b, another instance of the app, adopts the same build (staged
+// for the app already, so recorded under tcp-a only), is repointed to a
+// plain build without a restart, and the agent upgrades.
+func wClassifyStagedApp(t *testing.T, w *world) {
+ b := w.boxes[0]
+ if !w.provision(b, tcpA, tcpApp, "10.0.0.7:9101", vEngine) || !w.provision(b, tcpB, tcpApp, "10.0.0.7:9102", vPlain) {
+ t.Fatalf("witness: provisioning refused: %s", w.boxesState())
+ }
+ if !w.adopt(b, tcpB, vEngine, nil) {
+ t.Fatalf("witness: adoption of the declaring build refused: %s", w.boxesState())
+ }
+ w.check()
+ for _, v := range b.Staged[tcpB] {
+ if v == vEngine {
+ t.Fatalf("witness: the declaring build is recorded under %s too; the scenario needs it staged for the other instance only", tcpB)
+ }
+ }
+ if !w.repointAwake(b, tcpB, vPlain) {
+ t.Fatalf("witness: the awake repoint was refused: %s", w.boxesState())
+ }
+ w.check()
+ w.upgrade(b)
+ w.check()
+ w.requestBy(b, tcpB)
+ w.check()
+}
+
const (
tcpApp = "acct/tcp"
tcpA = "tcp-a"
@@ -789,6 +819,72 @@ func TestGrantRefusedOnUnresolvedMintedRow(t *testing.T) {
}
}
+// TestRevokeRunsTheBarrier: --grant on a minted row times out while the
+// edge's reload fails (1 written, not fenced); a public request is admitted
+// and held; --revoke through the minted row also times out and the mark stays
+// 1; adoption of a declaring build is refused for want of the fence. Once
+// the reload recovers, the fence drains the held request, the declaring
+// build starts, and nothing public reaches it. With the reload working,
+// --revoke returns once the edge has loaded it, and the mark is still 1.
+func TestRevokeRunsTheBarrier(t *testing.T) {
+ w := newWitness()
+ b := w.boxes[0]
+ w.tcpPublic(t, b)
+ b.ReloadFails = true
+ if w.grant(b, mintedB) {
+ t.Fatal("--grant returned loaded while the edge's reload fails")
+ }
+ if m := w.groupMark(b, tcpB); m != markInternal {
+ t.Fatalf("after the timed-out grant the group is %v, want internal", m)
+ }
+ held := w.admitPublic(b, tcpB, "/")
+ if held == nil {
+ t.Fatalf("the edge (still serving 0) refused a public request: %s", w.boxesState())
+ }
+ held.Held = true
+ if w.revoke(b, mintedB) {
+ t.Fatal("--revoke returned loaded while the edge's reload fails")
+ }
+ if b.Rows[mintedB].Grant {
+ t.Fatal("--revoke left the grant recorded")
+ }
+ if m := w.groupMark(b, tcpB); m != markInternal {
+ t.Fatalf("--revoke changed the mark to %v; only --public writes 0", m)
+ }
+ if w.adopt(b, tcpB, vEngine, nil) {
+ t.Fatalf("adoption of a declaring build passed the gate without a fence: %s", w.boxesState())
+ }
+ w.check()
+ b.ReloadFails = false
+ if !w.adopt(b, tcpB, vEngine, nil) {
+ t.Fatalf("adoption refused after the reload recovered: %s", w.boxesState())
+ }
+ w.check()
+ w.deliver(held)
+ if held.Reached != 0 {
+ t.Fatalf("the request admitted before the fence reached process %d", held.Reached)
+ }
+ for _, h := range []string{tcpB, mintedB} {
+ if r := w.admitPublic(b, h, "/"); r != nil {
+ t.Fatalf("a public request to %s was admitted after the fence", h)
+ }
+ }
+ // with the reload working, revoke waits for the edge and returns loaded
+ w.grant(b, tcpB)
+ if !w.revoke(b, tcpB) || b.Fenced < b.Wrote {
+ t.Fatalf("--revoke with a working reload did not return loaded: %s", w.boxesState())
+ }
+ if m := w.groupMark(b, tcpB); m != markInternal || !w.fenced(b, tcpB) {
+ t.Fatalf("after --revoke the group is %v (fenced %v), want internal and fenced", m, w.fenced(b, tcpB))
+ }
+ if len(w.violations) > 0 {
+ t.Fatalf("violations: %v", w.violations)
+ }
+ if t.Failed() {
+ dump(t, w)
+ }
+}
+
// wPublicHoldsLock: the engine was adopted to a plain build (the mark stays
// 1); the operator runs --public; after its checks pass and before it stops
// the group, adoption of a declaring build runs; then a public request