carlos / platform Public

enginesim: --public step 3 is its own rule; --grant refused on an unresolved minted row (#335 plan 1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 8c7d684fd2947e522518c8886a5732d65163900e parent 34c24ae
4 files changed, +122 −16
  • internal/sim/engines/classify_test.go +42 −13
  • internal/sim/engines/rules_test.go +2 −1
  • internal/sim/engines/witness_test.go +74 −2
  • internal/sim/engines/world_test.go +4 −0
diff --git a/internal/sim/engines/classify_test.go b/internal/sim/engines/classify_test.go
index 7ba31d27a..50ac8a968 100644
--- a/internal/sim/engines/classify_test.go
+++ b/internal/sim/engines/classify_test.go
@@ -189,12 +189,20 @@ func (w *world) grantLoaded(b *box, g string) string {
// grant is `carlos route --host <host> --grant doc-engine`: it records the
// grant, writes 1 on the group if the group now holds it, and returns once
-// the edge has loaded the change (or says it timed out).
+// the edge has loaded the change (or says it timed out). On a minted row
+// whose owner cannot be resolved it is refused: the CLI cannot find the
+// group to mark, and the row itself stays NULL and refused.
func (w *world) grant(b *box, host string) bool {
r := b.Rows[host]
if r == nil || r.Kind != "instance" || r.Grant {
return false
}
+ if followsOwner(r) {
+ if _, ok := b.resolveOwner(r); !ok {
+ w.logf("box%d: --grant doc-engine on %s: refused: the row's owner is not recorded and cannot be found uniquely; the row stays NULL and refused", b.ID, host)
+ return false
+ }
+ }
b.Wrote++
r.Grant, r.GrantAt = true, b.Wrote
g := w.group(b, host)
@@ -342,10 +350,12 @@ func (w *world) residualAtUpgrade(b *box) string {
// §5.1, "Becoming public is never automatic"). Holding the adoption lock:
// 1. refuse unless the live manifest is readable and does not declare
// (public-checks-live), and no row of the group holds the grant;
-// 2. stop the whole group, and
-// 3. refuse unless every cgroup of the group is empty (public-stops-group);
+// 2. stop the whole group (public-stops-group);
+// 3. refuse unless every cgroup of the group is empty (public-confirms-empty);
// 4. write 0 and release the lock.
//
+// Step 3 is what catches a stop that did not complete (w.clearStopMisses).
+//
// w.clearHook, if set, runs after step 1 ("locked") and after step 2
// ("stopped").
func (w *world) operatorPublic(b *box, g string) bool {
@@ -367,21 +377,39 @@ func (w *world) operatorPublic(b *box, g string) bool {
}
w.hook(b, g, "locked")
if on("public-stops-group") {
- w.stopGroup(b, g)
- w.logf("box%d: --public %s: the group is stopped", b.ID, g)
- w.hook(b, g, "stopped")
- if w.running(b, g) {
- w.logf("box%d: --public %s: refused: a cgroup of the group is not empty", b.ID, g)
- return false
- }
- } else {
- w.hook(b, g, "stopped")
+ w.clearStop(b, g)
+ }
+ w.hook(b, g, "stopped")
+ if on("public-confirms-empty") && w.running(b, g) {
+ w.logf("box%d: --public %s: refused: a cgroup of the group is not empty", b.ID, g)
+ return false
}
w.writeMark(b, g, markPublic)
w.logf("box%d: --public %s: 0 written; the lock is released", b.ID, g)
return true
}
+// clearStop is step 2: stop every process of group g, except, when
+// w.clearStopMisses is set, the first one, whose stop did not complete.
+func (w *world) clearStop(b *box, g string) {
+ var left *proc
+ if w.clearStopMisses {
+ for _, p := range b.Procs {
+ if p.Group == g {
+ left = p
+ break
+ }
+ }
+ }
+ w.stopGroup(b, g)
+ if left != nil {
+ b.Procs = append(b.Procs, left)
+ w.logf("box%d: --public %s: the stop did not complete: process %d (v%d) still runs", b.ID, g, left.ID, left.Ver)
+ return
+ }
+ w.logf("box%d: --public %s: the group is stopped", b.ID, g)
+}
+
func (w *world) hook(b *box, g, stage string) {
if w.clearHook != nil {
w.clearHook(b, g, stage)
@@ -455,8 +483,9 @@ func init() {
}
}
}
+ w.clearStopMisses = w.rng.Intn(4) == 0
w.operatorPublic(b, g)
- w.clearHook = nil
+ w.clearHook, w.clearStopMisses = nil, false
}},
action{name: "adopt-awake", weight: 2, run: func(w *world) {
b := w.pickBox()
diff --git a/internal/sim/engines/rules_test.go b/internal/sim/engines/rules_test.go
index a8a54911c..a022424d1 100644
--- a/internal/sim/engines/rules_test.go
+++ b/internal/sim/engines/rules_test.go
@@ -64,6 +64,7 @@ var allRules = []string{
"static-minted-public", // a minted static row is 0 by construction, never classified (R6-2)
"owner-nonempty-unique", // the owner fallback matches only non-empty backends, and only uniquely (R7-2)
"public-checks-live", // --public step 1: the live manifest is readable and does not declare
- "public-stops-group", // --public steps 2-3: stop the group, refuse unless its cgroups are empty (R7-1, R8-2)
+ "public-stops-group", // --public step 2: stop the whole group (R7-1, R8-2)
"public-holds-lock", // --public holds the adoption lock: the live link cannot move under it
+ "public-confirms-empty", // --public step 3: refuse unless every cgroup of the group is empty
}
diff --git a/internal/sim/engines/witness_test.go b/internal/sim/engines/witness_test.go
index 103b1e94d..5eace5b84 100644
--- a/internal/sim/engines/witness_test.go
+++ b/internal/sim/engines/witness_test.go
@@ -77,8 +77,10 @@ var witnesses = []witness{
"R7-2: two TCP instances of one app, the plain one's minted row minted before owners were recorded; a fallback that matches empty sockets puts the row in the engine's group, which marks it 1, so a public row never answers; with the rule, a match that is not unique is refused and logged, and INV-AV does not count it", wOwnerNonEmptyUnique},
{"public-checks-live", "public-checks-live", "INV-I1+INV-P",
"--public on a group whose live release declares; without step 1 the clear writes 0, and systemd's socket activation starts the declaring live build ungated (INV-I1) for a public request (INV-P)", wPublicChecksLive},
- {"public-stops-group", "public-stops-group", "INV-I1+INV-P",
- "R7-1/R8-2: a generation rollout repointed the live link to a plain build and only scheduled the swap, so the declaring generation still serves; a clear that neither stops the group nor checks its cgroups writes 0 under it", wPublicStopsGroup},
+ {"public-stops-group", "public-stops-group,public-confirms-empty", "INV-I1+INV-P",
+ "R7-1/R8-2: redundant alone: without the stop, step 3 finds the declaring generation still serving and refuses; with both gone, a clear after a rollout that repointed the live link to a plain build and only scheduled the swap writes 0 under the still-serving declaring generation", wPublicStopsGroup},
+ {"public-confirms-empty", "public-confirms-empty", "INV-I1+INV-P",
+ "the clear's stop does not complete and leaves the declaring generation running; without step 3 the clear writes 0 under it (INV-I1) and a public request reaches it (INV-P)", wPublicConfirmsEmpty},
{"public-holds-lock", "public-holds-lock", "INV-I1+INV-P",
"--public checks a plain live release; without the adoption lock, adoption repoints to a declaring build before the stop; the clear writes 0 over a declaring live link, and socket activation starts it ungated", wPublicHoldsLock},
}
@@ -717,6 +719,76 @@ func wPublicStopsGroup(t *testing.T, w *world) {
w.check()
}
+// wPublicConfirmsEmpty: the engine runs a declaring generation; a rollout of
+// a plain build repoints the live link and only schedules the swap; the
+// operator runs --public and its stop leaves that generation running; a
+// public request arrives.
+func wPublicConfirmsEmpty(t *testing.T, w *world) {
+ b := w.boxes[0]
+ if !w.adopt(b, eng, vEngine, nil) {
+ t.Fatalf("witness: adoption of the engine refused: %s", w.boxesState())
+ }
+ w.check()
+ if !w.repointAwake(b, eng, vPlain) {
+ t.Fatalf("witness: the rollout's repoint was refused: %s", w.boxesState())
+ }
+ w.check()
+ w.clearStopMisses = true
+ w.clearPublic(b, eng)
+ w.clearStopMisses = false
+ w.requestBy(b, engHost)
+ w.check()
+}
+
+// TestGrantRefusedOnUnresolvedMintedRow: --grant doc-engine on a minted row
+// whose owner is not recorded and cannot be found uniquely (here two
+// instances of its app share its address) is refused; the row stays NULL at
+// the edge and refused, and no group is marked.
+func TestGrantRefusedOnUnresolvedMintedRow(t *testing.T) {
+ w := newWitness()
+ b := w.boxes[0]
+ for _, h := range []string{tcpC, tcpD} {
+ if !w.provision(b, h, tcpApp, "10.0.0.7:9103", vPlain) {
+ t.Fatalf("provisioning %s refused: %s", h, w.boxesState())
+ }
+ }
+ w.mint(b, tcpC, mintedC, false)
+ // and one whose recorded owner has no row
+ b.Rows["gone"] = &row{Host: "gone", Kind: "instance", App: tcpApp, Backend: "10.0.0.7:9104", PathTo: map[string]string{}}
+ w.mint(b, "gone", "gone.acct.apps.example", true)
+ delete(b.Rows, "gone")
+ w.reload(b)
+ wrote := b.Wrote
+ for _, h := range []string{mintedC, "gone.acct.apps.example"} {
+ if w.grant(b, h) {
+ t.Errorf("--grant on %s, whose owner cannot be resolved, succeeded", h)
+ }
+ if b.Rows[h].Grant {
+ t.Errorf("--grant on %s recorded the grant", h)
+ }
+ if b.Serving[h] != markNull {
+ t.Errorf("the edge serves %s %v, want NULL", h, b.Serving[h])
+ }
+ if r := w.admitPublic(b, h, "/"); r != nil {
+ t.Errorf("a public request to %s was admitted", h)
+ }
+ }
+ if b.Wrote != wrote {
+ t.Errorf("a refused grant changed the registry (%d to %d)", wrote, b.Wrote)
+ }
+ for _, g := range []string{tcpC, tcpD} {
+ if m := w.groupMark(b, g); m != markPublic {
+ t.Errorf("group %s is %v after a refused grant, want public", g, m)
+ }
+ }
+ if len(w.violations) > 0 {
+ t.Errorf("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
diff --git a/internal/sim/engines/world_test.go b/internal/sim/engines/world_test.go
index 91a423520..83c5c7a41 100644
--- a/internal/sim/engines/world_test.go
+++ b/internal/sim/engines/world_test.go
@@ -141,6 +141,10 @@ type world struct {
// ("locked": step 1 passed; "stopped": step 2 done), so a scenario or the
// walk can interleave there.
clearHook func(b *box, g, stage string)
+ // clearStopMisses, when set, makes operatorPublic's stop leave one
+ // process of the group behind (a unit or generation whose stop the
+ // activator or control service failed to complete).
+ clearStopMisses bool
verbose bool
}