R4e (mesh-controller): deliver from the builder's keys, ask build-agent only for its own modules, follow the forge's protection for checks, announce the controller's states #441
@@ -0,0 +1,69 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// A new controller's own grants reach the bus before it writes (novox/hq issue 1800).
|
||||
//
|
||||
// A controller's grants are composed by the controller binary itself (broker.PermissionsFor), so the controller that
|
||||
// walks a delivery cannot put its successor's grants in the bus's user list: the list that carries them was sent only
|
||||
// once the new controller passed its gate on its first machine (carryUserList). Until then the new controller wrote
|
||||
// buckets its account was not granted, and the bus refused them: plans-building on 2026-10-21, declared-packages on
|
||||
// 2026-10-22, and R4e's states-announced would be the third.
|
||||
//
|
||||
// So a controller that starts, holding the lease and before it starts anything that writes its buckets, sends the
|
||||
// machine holding the bus its user list when the list it composes is not the one that machine was last sent: the grants
|
||||
// step's send (release.go grantsStep), every build there kept at the one it runs, refused unsent when the declaration
|
||||
// would change more than the user list (errNotGrantsOnly). No build of a running plan reaches that machine before the
|
||||
// plan's own tier sends it (ADR 0503 §8). A send that cannot be made is said once, and the controller serves on.
|
||||
//
|
||||
// It does not ask heldMachines, as carryUserList does: every reason that gives is a build that would move with the
|
||||
// list (a policy, a plan not yet sent there, no gate passed), and here none can. The running plan that brought this
|
||||
// controller holds the bus's machine until its tier sends it; asking would leave the list behind exactly when 1800
|
||||
// meets it. The refusal unsent (errNotGrantsOnly) is the guard instead.
|
||||
|
||||
// ownGrantsCarrier is what the start's send reads and does: whether the bus's list is behind and on which machine, and
|
||||
// the grants-only send to it. Variables so a test needs no store and no bus.
|
||||
type ownGrantsCarrier struct {
|
||||
behind func(ctx context.Context) (holder string, behind bool, err error)
|
||||
send func(ctx context.Context, holder string) error
|
||||
}
|
||||
|
||||
// carry sends the machine holding the bus the controller's own user list when it is behind, and says what it did.
|
||||
func (c ownGrantsCarrier) carry(ctx context.Context) string {
|
||||
holder, behind, err := c.behind(ctx)
|
||||
switch {
|
||||
case err != nil:
|
||||
return fmt.Sprintf("whether the bus's user list carries this controller's grants cannot be read, so it is not sent "+
|
||||
"at start; a bucket it newly writes may be refused until the gate carries it: %v", err)
|
||||
case holder == "" || !behind:
|
||||
return ""
|
||||
}
|
||||
if err := c.send(ctx, holder); err != nil {
|
||||
return fmt.Sprintf("this controller's grants could not be sent to %s, which holds the bus, at its start; a bucket "+
|
||||
"it newly writes may be refused until the gate carries them: %v", holder, err)
|
||||
}
|
||||
return fmt.Sprintf("sent %s, which holds the bus, this controller's user list alone, every build there kept, "+
|
||||
"before it writes", holder)
|
||||
}
|
||||
|
||||
// startWithOwnGrants carries the controller's own grants to the bus and only then starts what writes (then).
|
||||
func startWithOwnGrants(ctx context.Context, c ownGrantsCarrier, then func()) {
|
||||
if said := c.carry(ctx); said != "" {
|
||||
fmt.Println(said)
|
||||
}
|
||||
then()
|
||||
}
|
||||
|
||||
// ownGrantsOf is the carrier of the serving controller: the grants step's reading and its grants-only send.
|
||||
func ownGrantsOf(open *stores) ownGrantsCarrier {
|
||||
return ownGrantsCarrier{
|
||||
behind: func(ctx context.Context) (string, bool, error) { return brokerBehindOf(ctx, open, nil) },
|
||||
send: func(ctx context.Context, holder string) error {
|
||||
_, err := sendRollout(keepingEveryBuild(withScope(ctx, sendScope{}), holder), open, []string{holder})
|
||||
return err
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// 1800 (1): a controller starting with the bus's list behind its own sends the bus's machine once, before anything
|
||||
// that writes its buckets starts.
|
||||
func TestAStartingControllerSendsItsGrantsBeforeItWrites(t *testing.T) {
|
||||
var order []string
|
||||
c := ownGrantsCarrier{
|
||||
behind: func(context.Context) (string, bool, error) { return "novox", true, nil },
|
||||
send: func(_ context.Context, holder string) error { order = append(order, "send "+holder); return nil },
|
||||
}
|
||||
startWithOwnGrants(t.Context(), c, func() { order = append(order, "writers") })
|
||||
if strings.Join(order, ", ") != "send novox, writers" {
|
||||
t.Fatalf("the start did %v", order)
|
||||
}
|
||||
}
|
||||
|
||||
// 1800 (3), (4): no send when the list is current or there is no bus with a list; one that cannot be read or sent is
|
||||
// said once, and the controller goes on.
|
||||
func TestAStartingControllerSendsNothingWhenTheListIsCurrentAndGoesOnWhenRefused(t *testing.T) {
|
||||
sends := 0
|
||||
send := func(context.Context, string) error { sends++; return nil }
|
||||
for _, c := range []ownGrantsCarrier{
|
||||
{behind: func(context.Context) (string, bool, error) { return "novox", false, nil }, send: send},
|
||||
{behind: func(context.Context) (string, bool, error) { return "", true, nil }, send: send},
|
||||
} {
|
||||
if said := c.carry(t.Context()); said != "" || sends != 0 {
|
||||
t.Fatalf("a current list sent %d and said %q", sends, said)
|
||||
}
|
||||
}
|
||||
refused := ownGrantsCarrier{behind: func(context.Context) (string, bool, error) { return "novox", true, nil },
|
||||
send: func(context.Context, string) error { return errNotGrantsOnly }}
|
||||
went := false
|
||||
startWithOwnGrants(t.Context(), refused, func() { went = true })
|
||||
if !went {
|
||||
t.Fatal("a refused send stopped the controller")
|
||||
}
|
||||
if said := refused.carry(t.Context()); !strings.Contains(said, "could not be sent to novox") ||
|
||||
!strings.Contains(said, errNotGrantsOnly.Error()) {
|
||||
t.Fatalf("a refused send is said as %q", said)
|
||||
}
|
||||
unread := ownGrantsCarrier{behind: func(context.Context) (string, bool, error) { return "", false, errors.New("no store") },
|
||||
send: send}
|
||||
if said := unread.carry(t.Context()); !strings.Contains(said, "cannot be read") || sends != 0 {
|
||||
t.Fatalf("an unread list: sent %d, said %q", sends, said)
|
||||
}
|
||||
}
|
||||
|
||||
// 1800 (2): the start's send is the grants step's: composed with every build of the bus's machine kept at the one it
|
||||
// runs, so the declaration differs from its last only by the user list (and is refused unsent otherwise,
|
||||
// errNotGrantsOnly, release.go), whatever a running plan registered since.
|
||||
func TestAStartingControllersSendKeepsEveryBuildOfTheBusMachine(t *testing.T) {
|
||||
was := sendRollout
|
||||
t.Cleanup(func() { sendRollout = was })
|
||||
var kept bool
|
||||
var named []string
|
||||
sendRollout = func(ctx context.Context, _ *stores, names []string) ([]string, error) {
|
||||
kept, named = everyBuildKept(ctx, "novox"), names
|
||||
return names, nil
|
||||
}
|
||||
if err := ownGrantsOf(nil).send(t.Context(), "novox"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !kept || len(named) != 1 || named[0] != "novox" {
|
||||
t.Fatalf("the start's send keeps every build: %v, names %v", kept, named)
|
||||
}
|
||||
}
|
||||
@@ -189,7 +189,9 @@ func serve(ctx context.Context) (err error) {
|
||||
// Open plans move on events (novox/hq ADR 0406 §5, issue 974): a tier is asked on the `built` and the send of the
|
||||
// tier before it, a walk ends on its machines' reports, a silent machine is a key that expired, and a plan left
|
||||
// by a replaced controller resumes from the first look, which this asks for. No timer looks every so often.
|
||||
startRolloutEvents(ctx, open, server.JetStream())
|
||||
// **Its own grants on the bus before it writes** (novox/hq issue 1800): a bucket this controller newly writes is
|
||||
// granted before the first write, not after its gate.
|
||||
startWithOwnGrants(ctx, ownGrantsOf(open), func() { startRolloutEvents(ctx, open, server.JetStream()) })
|
||||
// And the merge window (novox/hq ADR 0276, ADR 0407): a batch is cut into its walk when the window closes,
|
||||
// which no merge or outcome says. The window is two keys on the bus that expire and a watcher that hears it;
|
||||
// nothing polls (issue 973).
|
||||
|
||||
Reference in New Issue
Block a user