Merge branch 'feat/47-G-fix9-builder2' into feat/47-G-fix9-checker
This commit is contained in:
@@ -528,7 +528,7 @@ func TestWantsAreWrittenAndNoDifferenceIs(t *testing.T) {
|
||||
// indexer its runtime of its own answers each first-view tool to the readers its manifest's callers lists name, at
|
||||
// its mark, and refuses every other account, the assigner's own included.
|
||||
firstViewsBeforeAnyNewFormState(t, b, map[string][]string{
|
||||
"assignments": {"n1.mesh-grants", "n1.node-tools", "n1.mesh-delivery-indexer"},
|
||||
"assignments": {"n1.mesh-grants", "n1.node-tools", "n1.mesh-delivery-indexer", "n1.mesh-delivery-builder"},
|
||||
"seat-holders": {"n1.node-tools"},
|
||||
"wants": {"n1.mesh-grants"},
|
||||
})
|
||||
|
||||
@@ -109,7 +109,7 @@ func TestManifest(t *testing.T) {
|
||||
t.Errorf("runs-as = %q: the bundle runs on the assigner's own account (ADR 0621 §1)", m.RunsAs)
|
||||
}
|
||||
// ADR 0621 §2: each first-view tool answers exactly its readers, never the assigner.
|
||||
callers := map[string]string{"assignments": "mesh-grants,node-tools,mesh-delivery-indexer", "seat-holders": "node-tools", "wants": "mesh-grants"}
|
||||
callers := map[string]string{"assignments": "mesh-grants,node-tools,mesh-delivery-indexer,mesh-delivery-builder", "seat-holders": "node-tools", "wants": "mesh-grants"}
|
||||
env := m.Build.Artifacts[0].Env
|
||||
for tool, list := range callers {
|
||||
if env[views.CallersEnv(tool)] != list || views.ParseCallers(env[views.CallersEnv(tool)]).Allows("n1."+m.Module) {
|
||||
|
||||
@@ -212,7 +212,7 @@
|
||||
"mesh-delivery-assigner"
|
||||
],
|
||||
"env": {
|
||||
"MESH_VIEW_CALLERS_ASSIGNMENTS": "mesh-grants,node-tools,mesh-delivery-indexer",
|
||||
"MESH_VIEW_CALLERS_ASSIGNMENTS": "mesh-grants,node-tools,mesh-delivery-indexer,mesh-delivery-builder",
|
||||
"MESH_VIEW_CALLERS_SEAT_HOLDERS": "node-tools",
|
||||
"MESH_VIEW_CALLERS_WANTS": "mesh-grants"
|
||||
}
|
||||
|
||||
@@ -130,15 +130,22 @@ func (g *ToolchainGate) placedTold() {
|
||||
// may not read it) it says so and the gate passes every report on: not fatal, since an indexer, builder or checker that
|
||||
// stops for it stops every delivery (issue 1945). An error of the watch after that ends on the channel.
|
||||
func FollowAssignments(ctx context.Context, js process.BucketOpener, gate assignmentsFollowed, changed func(context.Context), logf func(string, ...any)) (<-chan error, error) {
|
||||
return FollowAssignmentsIn(ctx, js, AssignmentsBucket, gate, changed, logf)
|
||||
}
|
||||
|
||||
// FollowAssignmentsIn is FollowAssignments for the state in bucket, the holder's `<holder>_assignments` where the holder is found at
|
||||
// run time (design 47 G A-119): the builder follows it so; FollowAssignments, with the fixed bucket, is the checker's until its own
|
||||
// change finds the holder.
|
||||
func FollowAssignmentsIn(ctx context.Context, js process.BucketOpener, bucket string, gate assignmentsFollowed, changed func(context.Context), logf func(string, ...any)) (<-chan error, error) {
|
||||
errs := make(chan error, 1)
|
||||
kv, err := js.KeyValue(ctx, AssignmentsBucket)
|
||||
kv, err := js.KeyValue(ctx, bucket)
|
||||
if err != nil {
|
||||
logf("the state %s cannot be read, so no node is counted by the assignments: %v", AssignmentsBucket, err)
|
||||
logf("the state %s cannot be read, so no node is counted by the assignments: %v", bucket, err)
|
||||
return errs, nil
|
||||
}
|
||||
w, err := kv.WatchAll(ctx, jetstream.IncludeHistory())
|
||||
if err != nil {
|
||||
logf("the state %s cannot be watched, so no node is counted by the assignments: %v", AssignmentsBucket, err)
|
||||
logf("the state %s cannot be watched, so no node is counted by the assignments: %v", bucket, err)
|
||||
return errs, nil
|
||||
}
|
||||
put := func(e jetstream.KeyValueEntry) bool {
|
||||
@@ -151,7 +158,7 @@ func FollowAssignments(ctx context.Context, js process.BucketOpener, gate assign
|
||||
return errs, ctx.Err()
|
||||
case e, ok := <-w.Updates():
|
||||
if !ok {
|
||||
return errs, errors.New("the watch of " + AssignmentsBucket + " ended before it settled")
|
||||
return errs, errors.New("the watch of " + bucket + " ended before it settled")
|
||||
}
|
||||
if e == nil {
|
||||
settled = true
|
||||
@@ -173,7 +180,7 @@ func FollowAssignments(ctx context.Context, js process.BucketOpener, gate assign
|
||||
return
|
||||
case e, ok := <-w.Updates():
|
||||
if !ok {
|
||||
errs <- errors.New("the watch of " + AssignmentsBucket + " ended")
|
||||
errs <- errors.New("the watch of " + bucket + " ended")
|
||||
return
|
||||
}
|
||||
if e != nil && put(e) {
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
|
||||
@@ -404,18 +403,6 @@ func main() {
|
||||
}
|
||||
return nil
|
||||
}, versions.HolderOf(branches))
|
||||
carriers := versions.CarriersFrom(versions.EntriesOf(branches))
|
||||
released := func(_ context.Context, module string) ([]meshcatalog.VersionEntry, error) {
|
||||
raw, err := stdio.Ask(catalogVersionsVerb, meshcatalog.VersionsInput{Module: module})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var a meshcatalog.VersionsAnswer
|
||||
if err := json.Unmarshal(raw, &a); err != nil {
|
||||
return nil, fmt.Errorf("the seat mesh-catalog's versions answered what does not read: %w", err)
|
||||
}
|
||||
return a.Versions, nil
|
||||
}
|
||||
reserve := func(name, version string) (meshbuilds.ReservedAnswer, error) {
|
||||
raw, err := stdio.Ask(reservedVerb, meshbuilds.ReservedInput{Name: name, Version: version})
|
||||
if err != nil {
|
||||
@@ -427,7 +414,7 @@ func main() {
|
||||
tools := Tools(r, put, askQueue(stdio.State(build.QueuesState), 25*time.Second), buildAgain, stdio.Log(builds.BuilderLog).Since, time.Now, vr, fr, ofr, reserve)
|
||||
tools = append(tools, buildTool(&build.BuildReader{Builds: read(stdio.State("builds")), Entry: entryOf(stdio.State("builds")), Asked: read(stdio.State("asked")),
|
||||
Newest: newestVia(stdio.Ask), Steps: r}))
|
||||
tools = append(tools, reservedTools(rr, buildAgain, time.Now, meshhandactsclient.Recorder{Ask: stdio.Ask}, carriers, released)...)
|
||||
tools = append(tools, reservedTools(rr, buildAgain, time.Now, meshhandactsclient.Recorder{Ask: stdio.Ask})...)
|
||||
qs, as := stdio.State(versions.PublishQueueState), stdio.State(versions.ActedState)
|
||||
tools = append(tools, republishTools(readerOf(qs), readerOf(as), buildAgain, time.Now, meshhandactsclient.Recorder{Ask: stdio.Ask})...)
|
||||
ws := stdio.State(build.WaitsState)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
@@ -14,31 +13,17 @@ import (
|
||||
"mesh-delivery-builder/internal/versions"
|
||||
)
|
||||
|
||||
// reservedTools are builder_reserved, builder_reserved_drop and the seat mesh-builds' verb reserved (design 47 G section
|
||||
// 369), served as tools of the bundle. The drop is a hand act: it is recorded in the hand-act log first, then written to
|
||||
// reservedTools are builder_reserved and builder_reserved_drop (design 47 G section 369), served as tools of the bundle. The drop is a hand act: it is recorded in the hand-act log first, then written to
|
||||
// reasks for the builder, which owns the reservations, to answer.
|
||||
//
|
||||
// The verb reserved given a name finds the module from the catalog half's view the bundle follows (carriers) and, for a version
|
||||
// no reservation holds, asks the seat mesh-catalog's verb versions (released); the builder's process serves no call (A-70).
|
||||
func reservedTools(rr versions.Reader, ask queueAsker, now func() time.Time, acts meshhandactsclient.Recorder, carriers func(context.Context, string) ([]versions.Built, error), released versions.CatalogVersions) []stdio.Tool {
|
||||
// The seat's verb reserved is the builder's process's (design 47 G A-115, A-127), not the bundle's.
|
||||
func reservedTools(rr versions.Reader, ask queueAsker, now func() time.Time, acts meshhandactsclient.Recorder) []stdio.Tool {
|
||||
str := func(args map[string]any, k string) string { s, _ := args[k].(string); return strings.TrimSpace(s) }
|
||||
return []stdio.Tool{
|
||||
{Name: versions.ReservedTool, Description: versions.ReservedDescription,
|
||||
Input: map[string]any{"type": "object", "properties": map[string]any{
|
||||
"module": map[string]any{"type": "string", "description": "only this module's reservations"}}},
|
||||
Run: func(args map[string]any) (any, error) { return versions.AnswerReserved(rr, str(args, "module")) }},
|
||||
{Name: versions.ReservedVerb, Description: "The seat mesh-builds' verb reserved: a module's reserved version with each artifact's digest once built " +
|
||||
"(design 47 G section 369), for a registry holder deciding whether to publish. Read-only.",
|
||||
Input: map[string]any{"type": "object", "properties": map[string]any{
|
||||
"module": map[string]any{"type": "string", "description": "the module; give this or name"},
|
||||
"name": map[string]any{"type": "string", "description": "a package's or an image's name in its ecosystem, in place of module"},
|
||||
"version": map[string]any{"type": "string", "description": "the version, when asked for a particular one"}}},
|
||||
Run: func(args map[string]any) (any, error) {
|
||||
if name := str(args, "name"); name != "" {
|
||||
return versions.AnswerReservedByName(context.Background(), rr, carriers, released, name, str(args, "version"))
|
||||
}
|
||||
return versions.AnswerReservedVerb(rr, str(args, "module"), str(args, "version"))
|
||||
}},
|
||||
{Name: versions.ReservedDropTool, Description: versions.ReservedDropDescription,
|
||||
Input: map[string]any{"type": "object", "required": []string{"module", "series", "why"}, "properties": map[string]any{
|
||||
"module": map[string]any{"type": "string", "description": "the module"},
|
||||
|
||||
@@ -1,41 +1,27 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
|
||||
"mesh-delivery-builder/internal/build"
|
||||
"mesh-delivery-builder/internal/versions"
|
||||
)
|
||||
|
||||
// Design 47 G §572, §582, A-70: the bundle's verb reserved takes a name and answers from the view it follows and the catalog's
|
||||
// versions, and builder_versions, asked by a name, calls reserved and answers what it answers.
|
||||
func TestReservedByNameIsTheBundlesAndBuilderVersionsCallsIt(t *testing.T) {
|
||||
// Design 47 G §572, §582, A-70: builder_versions, asked by a name, calls reserved and answers what it answers.
|
||||
func TestBuilderVersionsByNameCallsReserved(t *testing.T) {
|
||||
none := versions.Reader{Keys: func() ([]string, error) { return nil, nil }, Get: func(string) (json.RawMessage, bool, error) { return nil, false, nil }}
|
||||
carriers := func(_ context.Context, name string) ([]versions.Built, error) {
|
||||
if name == "@novox/mesh-sdk" {
|
||||
return []versions.Built{{Module: "mesh-sdk", Artifact: "ts"}}, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
released := func(_ context.Context, module string) ([]meshcatalog.VersionEntry, error) {
|
||||
return []meshcatalog.VersionEntry{{Module: module, Version: "0.1.2", Artifacts: []meshcatalog.VersionArtifact{{Artifact: "mesh-sdk:ts", Digest: "sha256:dd"}}}}, nil
|
||||
}
|
||||
acts := meshhandactsclient.Recorder{Ask: func(string, any) (json.RawMessage, error) { return json.RawMessage(`{}`), nil }}
|
||||
byName := map[string]func(map[string]any) (any, error){}
|
||||
for _, tl := range reservedTools(none, nil, time.Now, acts, carriers, released) {
|
||||
byName[tl.Name] = tl.Run
|
||||
}
|
||||
got, err := byName[versions.ReservedVerb](map[string]any{"name": "@novox/mesh-sdk", "version": "0.1.2"})
|
||||
want := meshbuilds.ReservedAnswer{Module: "mesh-sdk", Version: "0.1.2", Released: true, Artifacts: []meshbuilds.ReservedAnswerArtifactsItem{{Artifact: "mesh-sdk:ts", Digest: "sha256:dd"}}}
|
||||
if err != nil || got.(meshbuilds.ReservedAnswer).Released != want.Released || got.(meshbuilds.ReservedAnswer).Artifacts[0] != want.Artifacts[0] {
|
||||
t.Fatalf("reserved by name = %+v (%v), want %+v", got, err, want)
|
||||
// The bundle serves no verb reserved any more: the builder's process does (design 47 G A-115, A-127).
|
||||
acts := meshhandactsclient.Recorder{Ask: func(string, any) (json.RawMessage, error) { return json.RawMessage(`{}`), nil }}
|
||||
for _, tl := range reservedTools(none, nil, time.Now, acts) {
|
||||
if tl.Name == "reserved" {
|
||||
t.Error("the bundle serves the verb reserved, which the builder's process answers")
|
||||
}
|
||||
}
|
||||
var called []string
|
||||
reserve := func(name, version string) (meshbuilds.ReservedAnswer, error) {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
mma "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
||||
"git.novox.be/novox/mesh-sdk/go/seatlist"
|
||||
)
|
||||
|
||||
// assignerHolder is the module holding the seat mesh-module-assigner now, asked of the seat list through mesh-sdk's seatlist.HolderOf
|
||||
// until it answers or ctx ends (design 47 G A-119, A-126): the builder names no assigner module, so a new holder is followed with no
|
||||
// change of code. wait is the pause between two asks that failed; sleep says false when ctx ended during it.
|
||||
func assignerHolder(ctx context.Context, call seatlist.HolderCaller, wait time.Duration, sleep func(context.Context, time.Duration) bool, logf func(string, ...any)) (string, bool) {
|
||||
for {
|
||||
holder, err := seatlist.HolderOf(call, mma.Seat)
|
||||
if err == nil {
|
||||
return holder, true
|
||||
}
|
||||
logf("who holds the seat %s is not known yet, so no node is counted by the assignments: %v", mma.Seat, err)
|
||||
if !sleep(ctx, wait) {
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// assignmentsNames are the names made from the holder: the view's first-view tool `<holder>.assignments` and the
|
||||
// bucket `<holder>_assignments` of the state, both made from the holder found at run time.
|
||||
type assignmentsNames struct{ Holder, Tool, Bucket string }
|
||||
|
||||
func assignmentsOf(holder string) assignmentsNames {
|
||||
return assignmentsNames{Holder: holder, Tool: holder + "." + mma.StateAssignments, Bucket: holder + "_" + mma.StateAssignments}
|
||||
}
|
||||
|
||||
// sleepFor waits d, a context's bound, and says whether ctx is still alive.
|
||||
func sleepFor(ctx context.Context, d time.Duration) bool {
|
||||
w, cancel := context.WithTimeout(ctx, d)
|
||||
defer cancel()
|
||||
<-w.Done()
|
||||
return ctx.Err() == nil
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/seatlist"
|
||||
)
|
||||
|
||||
// The builder finds the assigner's holder at run time: asked again while the seat list does not answer, and the names of the view
|
||||
// are made from the holder (design 47 G A-119), not fixed in code.
|
||||
func TestTheAssignerHolderIsFoundAtRunTime(t *testing.T) {
|
||||
asks, slept := 0, 0
|
||||
call := func(key string, _ any) (json.RawMessage, error) {
|
||||
if key != seatlist.HolderKey() {
|
||||
t.Errorf("asked %q, want %q", key, seatlist.HolderKey())
|
||||
}
|
||||
if asks++; asks == 1 {
|
||||
return nil, errors.New("down")
|
||||
}
|
||||
return json.RawMessage(`{"seats":[{"seat":"mesh-module-assigner","holders":[{"module":"acme-assigner"}]}]}`), nil
|
||||
}
|
||||
holder, ok := assignerHolder(context.Background(), call, time.Second, func(context.Context, time.Duration) bool { slept++; return true }, func(string, ...any) {})
|
||||
if !ok || holder != "acme-assigner" || slept != 1 {
|
||||
t.Fatalf("holder = %q, %v after %d pauses, want acme-assigner after one", holder, ok, slept)
|
||||
}
|
||||
if n := assignmentsOf(holder); n.Tool != "acme-assigner.assignments" || n.Bucket != "acme-assigner_assignments" {
|
||||
t.Errorf("names = %+v", n)
|
||||
}
|
||||
if _, ok := assignerHolder(context.Background(), func(string, any) (json.RawMessage, error) { return nil, errors.New("down") }, time.Second,
|
||||
func(context.Context, time.Duration) bool { return false }, func(string, ...any) {}); ok {
|
||||
t.Error("a holder was found though ctx ended")
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
runner "git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep"
|
||||
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/ociartifactregistry"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
||||
@@ -58,6 +59,8 @@ accounts {
|
||||
}
|
||||
`
|
||||
|
||||
const testAssigner = "acme-assigner" // a holder of the seat mesh-module-assigner no code names
|
||||
|
||||
const (
|
||||
commit = "7a74cee83f88e6585a5511c3cc53bdf0f5303d20"
|
||||
fp = "src2:3333333333333333333333333333333333333333333333333333333333333333"
|
||||
@@ -288,7 +291,16 @@ func startBedWith(t *testing.T, closed bool) *bed {
|
||||
b.catalog.buildSeats(t, ctl, map[string]string{"node-build-go": testGo, "node-build-node": testNode, "node-build-image": testImage,
|
||||
"git": "mesh-delivery-forge-bridge", "mesh-catalog": "mesh-delivery-indexer"}) // the floor's sources are the holders of git and mesh-catalog
|
||||
readers := []string{"n1_" + builds.BuilderModule, "n2_" + builds.BuilderModule}
|
||||
viewstest.Serve(t, ctx, js, ctl, assignerModule, "assignments", assignerModule+".assigner_read_assignments", readers...)
|
||||
// The seat mesh-module-assigner's holder is read from the seat's verb assignments, as the builder finds it at run time (A-119).
|
||||
assignerSub, err := ctl.Subscribe("mesh.seat.mesh-module-assigner.call.assignments.>", func(m *nats.Msg) {
|
||||
reply, _ := json.Marshal(map[string]any{"result": map[string]any{"seats": []map[string]any{{"seat": "mesh-module-assigner", "holders": []map[string]string{{"module": testAssigner}}}}}, "node": "ctl"})
|
||||
_ = m.Respond(reply)
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = assignerSub.Unsubscribe() })
|
||||
viewstest.Serve(t, ctx, js, ctl, testAssigner, "assignments", testAssigner+".assignments", readers...)
|
||||
b.builds = mk(builds.BuilderBuildsBucket, builds.BuilderBuildsHistory)
|
||||
b.instances = mk(instancesBucket, 1)
|
||||
// The state bounds, whose keys have each its own time to live: an ask's step bounds (design 47 G A-77, A-118) are kept in it.
|
||||
@@ -545,6 +557,16 @@ func TestBuilderMakesADoneKeyWithTheDigests(t *testing.T) { // 1607/AC7
|
||||
if v.Toolchains["go"].Version != "go1.27.1" || v.Toolchains["typescript"].Version != "tsc5" {
|
||||
t.Errorf("toolchains = %+v", v.Toolchains)
|
||||
}
|
||||
// A-115: the builder's process answers the seat mesh-builds' verb reserved over the bus; the reservation of a module it never
|
||||
// built is answered in words (here that the bed's state reserved is not on the bus), never left unanswered.
|
||||
if conn, err := process.Connect(process.Account{URL: b.srvURL, Module: "mesh-delivery-view", User: "ctl", Password: "pw"}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
defer conn.Close()
|
||||
if _, err := conn.Call("seat:mesh-builds.reserved", meshbuilds.ReservedInput{Module: "never-built"}); err == nil || !strings.Contains(err.Error(), "_reserved") {
|
||||
t.Errorf("reserved of a module never built = %v, want the process's answer naming its state reserved", err)
|
||||
}
|
||||
}
|
||||
// ADR 0618 §1: the done key names the reference forms it is written in.
|
||||
if len(v.Needs) != 1 || v.Needs[0] != "form:artifact-store" {
|
||||
t.Errorf("needs = %v, want the first form", v.Needs)
|
||||
|
||||
@@ -120,10 +120,6 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
// catalog half's claims, found at each use.
|
||||
seatGate := newSeatGate()
|
||||
seats := buildrun.NewBuildSeats(seatGate.definitions, versions.HolderOf(branches))
|
||||
assignments, err := reader.Follow(assignerModule, "assignments", assignerModule+".assigner_read_assignments")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bound := defaultStepBound
|
||||
if v := strings.TrimSpace(getenv("MESH_BUILDER_STEP_BOUND")); v != "" {
|
||||
if bound, err = time.ParseDuration(v); err != nil {
|
||||
@@ -245,8 +241,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
decided := &decisions{subject: oc.SubjectDecided(cred.Module)}
|
||||
retakeActed := awaitedReserved{awaitedVersions{b: actedKV, bucket: versions.ActedBucket}}
|
||||
asker := &operatorask.Asker[oc.Warrant]{Module: cred.Module, Call: calls.Call, Record: calls.Ask, Publish: publishVia(ctx, js), Subscribe: decided.hear(logger.Printf),
|
||||
Contract: operatorask.Contract{Lists: "seat:" + mma.Seat + "." + mma.VerbAssignments, ListBody: mma.AssignmentsInput{Seats: mma.AssignmentsInputSeatsTrue},
|
||||
Seat: oc.Seat, StateAsks: oc.StateAsks},
|
||||
Contract: operatorask.Contract{Seat: oc.Seat, StateAsks: oc.StateAsks},
|
||||
AskSubject: oc.AcceptSubjectAsk, DecidedSubject: oc.SubjectDecided}
|
||||
operator := &build.OperatorAsk{Builder: b, Acted: retakeActed, Ctx: ctx, Logf: logger.Printf, Asker: asker,
|
||||
Client: &occ.Client{Module: cred.Module, State: calls.State("asked"),
|
||||
@@ -348,15 +343,26 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
// asked when the bound ends), is call-unanswered in the builder's findings (issue 1966, ADR 0668 §3, ADR 0596 §2).
|
||||
reads.callsOf(found, pubBoundsKV.KV, heardBy(calls.CallKept), logger.Printf)
|
||||
b.Forms = reads.built
|
||||
// The assignments names the nodes the builder counts, once its first view is answered.
|
||||
// The assignments names the nodes the builder counts, once the seat list names the seat's holder and its first view is answered:
|
||||
// the view is the holder's first-view tool, followed by its views family (A-119, A-131); no assigner module is named.
|
||||
assignmentsErrs := make(chan error, 1)
|
||||
go func() {
|
||||
holder, ok := assignerHolder(ctx, calls.Call, 5*time.Second, sleepFor, logger.Printf)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
names := assignmentsOf(holder)
|
||||
assignments, err := reader.Follow(names.Holder, mma.StateAssignments, names.Tool)
|
||||
if err != nil {
|
||||
assignmentsErrs <- err
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-assignments.Follower().Ready():
|
||||
}
|
||||
pe, err := buildrun.FollowAssignments(ctx, reader, gate, b.NodesChanged, logger.Printf)
|
||||
pe, err := buildrun.FollowAssignmentsIn(ctx, reader, names.Bucket, gate, b.NodesChanged, logger.Printf)
|
||||
if err != nil {
|
||||
assignmentsErrs <- err
|
||||
return
|
||||
@@ -397,6 +403,14 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
// modules again (design 47 G A-82, §204).
|
||||
// The runner's build step ask, the builder's one entry point for a candidate (design 47 G A-97).
|
||||
b.StepEntry = newEntryWaits(branches, half.manifests).entry
|
||||
// The seat mesh-builds' verb reserved, answered here (A-115, A-127): the reservations, the catalog half's carriers and the seat
|
||||
// mesh-catalog's versions.
|
||||
stopReserved, err := calls.AnswerSeat(meshbuilds.Seat, map[string]process.SeatHandler{
|
||||
meshbuilds.VerbReserved: reservedVerb(ctx, reservedReader(ctx, reserved.r.Store), versions.CarriersFrom(versions.EntriesOf(branches)), catalogReleased(calls.Call))})
|
||||
if err != nil {
|
||||
return fmt.Errorf("answering the verb reserved: %w", err)
|
||||
}
|
||||
defer stopReserved()
|
||||
if err := serveSteps(ctx, conn, js, cred, b, logger.Printf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package main
|
||||
|
||||
import (
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
||||
)
|
||||
|
||||
// The holders and buckets the builder names, taken from its own manifest (module.json: its `reads`) and from the holders'
|
||||
@@ -15,8 +14,6 @@ const (
|
||||
|
||||
// indexerModule is the holder of the states modules, manifests, candidates and packages the builder follows.
|
||||
indexerModule = "mesh-delivery-indexer"
|
||||
// assignerModule is the holder of the state assignments.
|
||||
assignerModule = buildrun.AssignerModule
|
||||
)
|
||||
|
||||
// namedTreePrefix opens the tree input of a module whose every artifact names its paths.
|
||||
|
||||
@@ -38,15 +38,14 @@ func TestTheNamesAreTheManifests(t *testing.T) {
|
||||
t.Errorf("the builder follows %s, an indexer state, its manifest consumes it", c)
|
||||
}
|
||||
}
|
||||
for _, view := range []string{"mesh-catalog.catalog-revision", "mesh-catalog.registration-changed", "mesh-delivery.source-moved", assignerModule + ".views.assignments"} {
|
||||
for _, view := range []string{"mesh-catalog.catalog-revision", "mesh-catalog.registration-changed", "mesh-delivery.source-moved", "seat:mesh-module-assigner.views.assignments"} {
|
||||
if !slices.Contains(m.Consumes, view) {
|
||||
t.Errorf("the builder follows %s, its manifest consumes %v", view, m.Consumes)
|
||||
}
|
||||
}
|
||||
for want, got := range map[string]string{
|
||||
"mesh-build-go_steps": buildrun.StepsBucket("mesh-build-go"),
|
||||
buildrun.ToolchainsHeardBucket: "mesh-delivery-builder_toolchains-heard",
|
||||
"mesh-delivery-assigner_assignments": buildrun.AssignmentsBucket,
|
||||
"mesh-build-go_steps": buildrun.StepsBucket("mesh-build-go"),
|
||||
buildrun.ToolchainsHeardBucket: "mesh-delivery-builder_toolchains-heard",
|
||||
} {
|
||||
if want != got {
|
||||
t.Errorf("a bucket is %q, want %q", got, want)
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-sdk/go/process"
|
||||
|
||||
"mesh-delivery-builder/internal/versions"
|
||||
)
|
||||
|
||||
// The seat mesh-builds' verb reserved is answered by the builder's own process (design 47 G A-115, A-127; A-87: builder_build stays
|
||||
// its tools bundle's): a module's reserved version with each artifact's digest once built, for a registry holder deciding whether to
|
||||
// publish. Given a name it finds the module from the catalog half the process follows (carriers) and, for a version no reservation
|
||||
// holds, asks the seat mesh-catalog's verb versions. The process's manifest declares the verb in `serves` beside its process.
|
||||
|
||||
// reservedReader is the state reserved read through its store, as the verb's answers read it.
|
||||
func reservedReader(ctx context.Context, store versions.Store) versions.Reader {
|
||||
return versions.Reader{
|
||||
Keys: func() ([]string, error) { return store.KeysOf(ctx, versions.AllFilter) },
|
||||
Get: func(key string) (json.RawMessage, bool, error) {
|
||||
v, _, found, err := store.Get(ctx, key)
|
||||
return v, found, err
|
||||
}}
|
||||
}
|
||||
|
||||
// reservedVerb is the handler of the verb reserved.
|
||||
func reservedVerb(ctx context.Context, rr versions.Reader, carriers func(context.Context, string) ([]versions.Built, error), released versions.CatalogVersions) process.SeatHandler {
|
||||
return func(_ string, body json.RawMessage) (any, error) {
|
||||
var in meshbuilds.ReservedInput
|
||||
if len(body) > 0 {
|
||||
if err := json.Unmarshal(body, &in); err != nil {
|
||||
return nil, fmt.Errorf("the arguments do not fit the verb reserved: %w", err)
|
||||
}
|
||||
}
|
||||
if name := strings.TrimSpace(in.Name); name != "" {
|
||||
return versions.AnswerReservedByName(ctx, rr, carriers, released, name, in.Version)
|
||||
}
|
||||
return versions.AnswerReservedVerb(rr, in.Module, in.Version)
|
||||
}
|
||||
}
|
||||
|
||||
// catalogReleased are a module's released versions, asked of the seat mesh-catalog's verb versions through call.
|
||||
func catalogReleased(call toolCaller) versions.CatalogVersions {
|
||||
return func(_ context.Context, module string) ([]meshcatalog.VersionEntry, error) {
|
||||
raw, err := call("seat:"+meshcatalog.Seat+".versions", meshcatalog.VersionsInput{Module: module})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var a meshcatalog.VersionsAnswer
|
||||
if err := json.Unmarshal(raw, &a); err != nil {
|
||||
return nil, fmt.Errorf("the seat mesh-catalog's versions answered what does not read: %w", err)
|
||||
}
|
||||
return a.Versions, nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
|
||||
"mesh-delivery-builder/internal/versions"
|
||||
)
|
||||
|
||||
// Design 47 G A-115, A-127: the builder's process answers the verb reserved, given a name, from the catalog half's carriers and the
|
||||
// seat mesh-catalog's versions, and given a module from its reservations.
|
||||
func TestTheProcessAnswersReserved(t *testing.T) {
|
||||
none := versions.Reader{Keys: func() ([]string, error) { return nil, nil }, Get: func(string) (json.RawMessage, bool, error) { return nil, false, nil }}
|
||||
carriers := func(_ context.Context, name string) ([]versions.Built, error) {
|
||||
if name == "@novox/mesh-sdk" {
|
||||
return []versions.Built{{Module: "mesh-sdk", Artifact: "ts"}}, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
var asked []string
|
||||
released := catalogReleased(func(key string, _ any) (json.RawMessage, error) {
|
||||
asked = append(asked, key)
|
||||
return json.Marshal(meshcatalog.VersionsAnswer{Versions: []meshcatalog.VersionEntry{{Module: "mesh-sdk", Version: "0.1.2",
|
||||
Artifacts: []meshcatalog.VersionArtifact{{Artifact: "mesh-sdk:ts", Digest: "sha256:dd"}}}}})
|
||||
})
|
||||
handle := reservedVerb(context.Background(), none, carriers, released)
|
||||
got, err := handle("", json.RawMessage(`{"name":"@novox/mesh-sdk","version":"0.1.2"}`))
|
||||
a, ok := got.(meshbuilds.ReservedAnswer)
|
||||
if err != nil || !ok || !a.Released || a.Module != "mesh-sdk" || len(a.Artifacts) != 1 || a.Artifacts[0].Digest != "sha256:dd" {
|
||||
t.Fatalf("reserved by name = %+v (%v)", got, err)
|
||||
}
|
||||
if len(asked) != 1 || asked[0] != "seat:mesh-catalog.versions" {
|
||||
t.Errorf("the released versions were asked of %v, want the seat mesh-catalog's versions", asked)
|
||||
}
|
||||
if _, err := handle("", json.RawMessage(`{"module":"mesh-sdk"}`)); err == nil {
|
||||
t.Error("a module with no reservation was answered")
|
||||
}
|
||||
if _, err := handle("", json.RawMessage(`{"module":`)); err == nil {
|
||||
t.Error("arguments that are no JSON were answered")
|
||||
}
|
||||
}
|
||||
@@ -118,6 +118,22 @@ type Republisher struct {
|
||||
mu sync.Mutex
|
||||
runs map[string]map[string][]string // node -> holder module -> the registry seats it claims
|
||||
busy map[string]bool
|
||||
// lastQueued is the newest time a version was queued at: a clock that has not moved between two versions (a coarse clock,
|
||||
// or a fixed one) must not make the queue's order the order of its keys, so each is queued after the last.
|
||||
lastQueued time.Time
|
||||
}
|
||||
|
||||
// queuedAt is the time a version is queued at: now, or a nanosecond after the last one queued when now is not after it, so the
|
||||
// order the versions were queued in (newest reservation first) is the order they are sent in.
|
||||
func (r *Republisher) queuedAt() time.Time {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
at := r.now()
|
||||
if !at.After(r.lastQueued) {
|
||||
at = r.lastQueued.Add(time.Nanosecond)
|
||||
}
|
||||
r.lastQueued = at
|
||||
return at
|
||||
}
|
||||
|
||||
func (r *Republisher) now() time.Time {
|
||||
@@ -349,7 +365,7 @@ func (r *Republisher) enqueue(ctx context.Context, v ReservedVersion, holder str
|
||||
} else if found {
|
||||
return false, nil
|
||||
}
|
||||
raw, _ := json.Marshal(QueuedPublish{V: 1, Holder: holder, Module: v.Module, Version: v.Version, Artifacts: arts, Queued: r.now()})
|
||||
raw, _ := json.Marshal(QueuedPublish{V: 1, Holder: holder, Module: v.Module, Version: v.Version, Artifacts: arts, Queued: r.queuedAt()})
|
||||
used, err := r.Queue.Bytes(ctx)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("the size of %s cannot be read: %w", PublishQueueBucket, err)
|
||||
|
||||
@@ -119,8 +119,8 @@ func TestManifestDeclarations(t *testing.T) { // 1607/AC1
|
||||
// 1887/A4c: the builder chooses no node, so it reads neither presence nor have, invokes no build module's tool and
|
||||
// holds no capacity; it puts asks on the three build seats and reads their steps (ADR 0525 §1, §4, §5). Whether a
|
||||
// node's machine is heard it asks the watchdogs seat's presence (issue 1966, ADR 0596 §2, ADR 0668 §3).
|
||||
if want := []string{"mesh-delivery-assigner.assignments",
|
||||
"node-build-go.steps", "node-build-image.steps", "node-build-node.steps"}; !reflect.DeepEqual(sorted(m.Reads), want) {
|
||||
if want := []string{"node-build-go.steps", "node-build-image.steps", "node-build-node.steps",
|
||||
"seat:mesh-module-assigner.assignments"}; !reflect.DeepEqual(sorted(m.Reads), want) {
|
||||
t.Errorf("reads = %v, want %v", m.Reads, want)
|
||||
}
|
||||
if want := []string{"node-build-go", "node-build-image", "node-build-node", "operator-channel"}; !reflect.DeepEqual(sorted(m.Uses), want) {
|
||||
@@ -133,15 +133,15 @@ func TestManifestDeclarations(t *testing.T) { // 1607/AC1
|
||||
// read; the catalog half (the modules, their manifests, their packages and the package names a package is numbered under) is
|
||||
// read through the seat mesh-catalog's branch follower (design 47 G §348, §614), and the indexer's states not at all.
|
||||
for _, r := range m.Reads {
|
||||
// The toolchain gate follows the assigner's assignments through meshmoduleassignerclient (buildrun.AssignmentsBucket), so
|
||||
// it declares that one read, as the console and route-proxy do; everything else of theirs is asked by tools.
|
||||
if strings.HasPrefix(r, "mesh-delivery-indexer.") || (strings.HasPrefix(r, "mesh-delivery-assigner.") && r != "mesh-delivery-assigner.assignments") {
|
||||
// The toolchain gate follows the assigner's assignments in the bucket of the seat's holder, so it declares that one read by the
|
||||
// seat (A-116, A-119); no module is named and everything else of the assigner's is asked by tools.
|
||||
if strings.HasPrefix(r, "mesh-delivery-indexer.") || strings.HasPrefix(r, "mesh-delivery-assigner.") {
|
||||
t.Errorf("reads %s: the indexer's and the assigner's state is read by their tools and views families alone", r)
|
||||
}
|
||||
}
|
||||
// Design 51 *The floor*: the floor's three sources, the registry seats' versions, git.tags and indexer_requirements.
|
||||
// The watchdogs seat's presence says a node's machine is heard (issue 1966, ADR 0596 §2, ADR 0668 §3).
|
||||
if want := []string{"mesh-delivery-assigner.assigner_read_assignments", "seat:mesh-catalog.entry", "seat:mesh-catalog.registrations", "seat:mesh-catalog.seat-definitions", "seat:mesh-catalog.revision", "seat:mesh-delivery.source", "seat:container-image-registry.publish", "seat:container-image-registry.remove", "seat:container-image-registry.versions",
|
||||
if want := []string{"seat:mesh-catalog.entry", "seat:mesh-catalog.registrations", "seat:mesh-catalog.seat-definitions", "seat:mesh-catalog.revision", "seat:mesh-delivery.source", "seat:container-image-registry.publish", "seat:container-image-registry.remove", "seat:container-image-registry.versions",
|
||||
"seat:git.tag", "seat:git.tags",
|
||||
"seat:go-package-registry.publish", "seat:go-package-registry.remove", "seat:go-package-registry.versions", "seat:mesh-artifact-sweep.held", "seat:mesh-builds.reserved", "seat:mesh-catalog.versions", "seat:mesh-hand-acts.hand-act", "seat:mesh-module-assigner.assignments", "seat:mesh-watchdogs.presence", "seat:node-build-go.drop", "seat:node-build-image.drop", "seat:node-build-node.drop", "seat:node-engine.manifest-reads", "seat:npm-package-registry.publish", "seat:npm-package-registry.remove",
|
||||
"seat:npm-package-registry.versions", "seat:oci-artifact-registry.publish", "seat:oci-artifact-registry.remove", "seat:oci-artifact-registry.versions"}; !reflect.DeepEqual(sorted(m.Invokes), sorted(want)) {
|
||||
@@ -156,7 +156,7 @@ func TestManifestDeclarations(t *testing.T) { // 1607/AC1
|
||||
if want := []string{"container-image-registry.kept", "container-image-registry.published", "container-image-registry.refused", "container-image-registry.removed",
|
||||
"go-package-registry.kept", "go-package-registry.published", "go-package-registry.refused", "go-package-registry.removed",
|
||||
"mesh-artifact-sweep.artifacts-gone", "mesh-catalog.manifest-accepted", "mesh-catalog.seat-definition-changed", "mesh-catalog.seat-definition-removed", "mesh-catalog.version-released", "mesh-catalog.versions-filled",
|
||||
"mesh-conditions.condition-cleared", "mesh-conditions.findings-asked", "mesh-delivery-assigner.views.assignments",
|
||||
"mesh-conditions.condition-cleared", "mesh-conditions.findings-asked", "seat:mesh-module-assigner.views.assignments",
|
||||
"mesh-catalog.catalog-revision", "mesh-catalog.registration-changed", "mesh-delivery.source-moved", "node-build-go.toolchain-reported", "node-build-node.toolchain-reported", "node-engine.manifest-reads-changed", "node-engine.running",
|
||||
"npm-package-registry.kept", "npm-package-registry.published", "npm-package-registry.refused", "npm-package-registry.removed",
|
||||
"oci-artifact-registry.kept", "oci-artifact-registry.published", "oci-artifact-registry.refused", "oci-artifact-registry.removed", "operator-channel.decided"}; !reflect.DeepEqual(sorted(m.Consumes), sorted(want)) {
|
||||
@@ -170,7 +170,7 @@ func TestManifestDeclarations(t *testing.T) { // 1607/AC1
|
||||
}
|
||||
// 1945: the builder's tools (builder_seat_queue: note 62), served by its tools bundle, and the event it says when it asks an ask again.
|
||||
if want := []string{"builder_announce_again", "builder_ask_again", "builder_bounds", "builder_build", "builder_build_again", "builder_builds_since", "builder_engine_reads", "builder_first_view_again", "builder_first_views",
|
||||
"builder_floor_again", "builder_floor_waits", "builder_old_form", "builder_publish_again", "builder_publish_queue", "builder_release", "builder_reserved", "builder_reserved_drop", "builder_seat_list", "builder_seat_list_again", "builder_seat_queue", "builder_stuck", "builder_toolchains", "builder_toolchains_drop", "builder_versions", "builder_waits", "reserved"}; !reflect.DeepEqual(sorted(m.Tools), want) {
|
||||
"builder_floor_again", "builder_floor_waits", "builder_old_form", "builder_publish_again", "builder_publish_queue", "builder_release", "builder_reserved", "builder_reserved_drop", "builder_seat_list", "builder_seat_list_again", "builder_seat_queue", "builder_stuck", "builder_toolchains", "builder_toolchains_drop", "builder_versions", "builder_waits"}; !reflect.DeepEqual(sorted(m.Tools), want) {
|
||||
t.Errorf("tools = %v, want %v", m.Tools, want)
|
||||
}
|
||||
// Design 47 G §672: the toolchain waits' stream, kept 7 days within 16 MiB, and the bound's setting, 60 minutes.
|
||||
@@ -195,6 +195,19 @@ func TestManifestDeclarations(t *testing.T) { // 1607/AC1
|
||||
if len(m.Claims) != 1 || m.Claims[0].Name != "mesh-builds" || m.Claims[0].Scope != "mesh" || !reflect.DeepEqual(m.Claims[0].Serves, []string{"reserved"}) {
|
||||
t.Errorf("claims = %+v, want mesh-builds, scope mesh, serving reserved", m.Claims)
|
||||
}
|
||||
// A-115: the process answers the verb itself and says so in `serves` beside its process, with no `bus`; the bundle serves no tool named so.
|
||||
var resources []map[string]any
|
||||
if err := json.Unmarshal(top["resources"], &resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, r := range resources {
|
||||
if r["type"] == "process" && !reflect.DeepEqual(r["serves"], []any{"seat:mesh-builds.reserved"}) {
|
||||
t.Errorf("the process serves %v, want seat:mesh-builds.reserved (A-115)", r["serves"])
|
||||
}
|
||||
}
|
||||
if slices.Contains(m.Tools, "reserved") {
|
||||
t.Error("the tools bundle serves reserved, which the process answers")
|
||||
}
|
||||
for _, k := range []string{"provides", "requires", "defines-seats", "seat", "seats", "bus"} {
|
||||
if _, ok := top[k]; ok {
|
||||
t.Errorf("the manifest declares %q: the builder has none", k)
|
||||
|
||||
@@ -35,7 +35,6 @@
|
||||
"builder_toolchains_drop",
|
||||
"builder_waits",
|
||||
"builder_versions",
|
||||
"reserved",
|
||||
"builder_first_views",
|
||||
"builder_first_view_again"
|
||||
],
|
||||
@@ -73,7 +72,7 @@
|
||||
"oci-artifact-registry.published",
|
||||
"oci-artifact-registry.refused",
|
||||
"oci-artifact-registry.removed",
|
||||
"mesh-delivery-assigner.views.assignments",
|
||||
"seat:mesh-module-assigner.views.assignments",
|
||||
"mesh-catalog.catalog-revision",
|
||||
"mesh-catalog.registration-changed",
|
||||
"mesh-delivery.source-moved"
|
||||
@@ -99,7 +98,6 @@
|
||||
"seat:npm-package-registry.versions",
|
||||
"seat:oci-artifact-registry.remove",
|
||||
"seat:oci-artifact-registry.versions",
|
||||
"mesh-delivery-assigner.assigner_read_assignments",
|
||||
"seat:container-image-registry.publish",
|
||||
"seat:go-package-registry.publish",
|
||||
"seat:npm-package-registry.publish",
|
||||
@@ -160,7 +158,7 @@
|
||||
"node-build-go.steps",
|
||||
"node-build-image.steps",
|
||||
"node-build-node.steps",
|
||||
"mesh-delivery-assigner.assignments"
|
||||
"seat:mesh-module-assigner.assignments"
|
||||
],
|
||||
"uses": [
|
||||
"node-build-go",
|
||||
@@ -209,6 +207,9 @@
|
||||
"type": "process",
|
||||
"name": "mesh-delivery-builder",
|
||||
"artifact": "program",
|
||||
"serves": [
|
||||
"seat:mesh-builds.reserved"
|
||||
],
|
||||
"run": [
|
||||
"./mesh-delivery-builder"
|
||||
],
|
||||
|
||||
@@ -55,6 +55,7 @@ type manifest struct {
|
||||
OwnSecrets map[string]string `json:"own-secrets"`
|
||||
SecretsOwner string `json:"secrets-owner"`
|
||||
Resources []map[string]any `json:"resources"`
|
||||
Requires []require `json:"requires"`
|
||||
Build struct {
|
||||
Artifacts []artifact `json:"artifacts"`
|
||||
} `json:"build"`
|
||||
@@ -93,7 +94,7 @@ func TestManifestDeclarations(t *testing.T) { // 1240/AC1
|
||||
// answers is the teller's answer to each ask; asked is what it asked of the router, which condition it raised and which
|
||||
// gate key and revision each ask was made at (issue 1343); ends is the ends asked by hand with teller_end_ask (issue 774).
|
||||
// first-views is where its first view of the assigner's asks stands and findings its condition past the bound (ADR 0615).
|
||||
if want := []state{{Name: "answers", History: 5}, {Name: "asked", History: 1}, {Name: "ends", History: 1}, {Name: "first-views", History: 1}, {Name: "findings", History: 1}}; !reflect.DeepEqual(m.State, want) {
|
||||
if want := []state{{Name: "answers", History: 5}, {Name: "asked", History: 1}, {Name: "ends", History: 1}, {Name: "first-views", History: 1}}; !reflect.DeepEqual(m.State, want) {
|
||||
t.Errorf("state = %+v, want %+v", m.State, want)
|
||||
}
|
||||
// The view's answers to the assigner's asks are read so the view's answer fills an ask that needs no proof (1297/B10); the
|
||||
@@ -133,14 +134,17 @@ func TestManifestDeclarations(t *testing.T) { // 1240/AC1
|
||||
t.Errorf("emits = %v, want %v", m.Emits, want)
|
||||
}
|
||||
// design 47 G §383, §387: the stream <module>-findings, 7 days and 16 MiB; §368: version is MAJOR.MINOR.
|
||||
if want := []stream{{Name: "mesh-delivery-teller-findings", Events: []string{"finding-written.*", "finding-cleared.*", "findings-stated.*"}, MaxAgeDays: 7, MaxMiB: 16}}; !reflect.DeepEqual(m.Streams, want) {
|
||||
t.Errorf("streams = %+v, want %+v", m.Streams, want)
|
||||
if len(m.Streams) != 0 {
|
||||
t.Errorf("streams = %+v: the finding stream is a require now", m.Streams)
|
||||
}
|
||||
if want := findingRequires(1); !reflect.DeepEqual(m.Requires, want) {
|
||||
t.Errorf("requires = %+v, want %+v", m.Requires, want)
|
||||
}
|
||||
if m.Version != "0.1" {
|
||||
t.Errorf("version = %q, want 0.1", m.Version)
|
||||
}
|
||||
// No bus or seat (ADR 0437 §4).
|
||||
for _, k := range []string{"logs", "claims", "provides", "requires", "defines-seats", "seat", "seats", "bus"} {
|
||||
for _, k := range []string{"logs", "claims", "provides", "defines-seats", "seat", "seats", "bus"} {
|
||||
if _, ok := top[k]; ok {
|
||||
t.Errorf("the manifest declares %q: the teller has none", k)
|
||||
}
|
||||
@@ -243,3 +247,22 @@ func TestNoPartHandWritesItsBusConnection(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// require is one entry of the manifest's requires: a provision the module asks for by a template (ADR 0595), which the
|
||||
// controller's reader takes.
|
||||
type require struct {
|
||||
Name string `json:"name"`
|
||||
Storage string `json:"storage"`
|
||||
Template string `json:"template"`
|
||||
Options map[string]any `json:"options"`
|
||||
}
|
||||
|
||||
// findingRequires is the findings state and the finding stream a module asks for by their templates (design 47 G
|
||||
// §383, §387; A-30): 7 days and 16 MiB, the findings state keeping the given history.
|
||||
func findingRequires(history float64) []require {
|
||||
return []require{
|
||||
{Name: "findings", Storage: "key-value", Template: "state", Options: map[string]any{"history": history}},
|
||||
{Name: "finding-events", Storage: "stream", Template: "events", Options: map[string]any{
|
||||
"events": []any{"finding-written.*", "finding-cleared.*", "findings-stated.*"}, "max-age-days": float64(7), "max-mib": float64(16)}},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,12 +15,15 @@ import (
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient/enginereads"
|
||||
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient"
|
||||
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshwatchdogsclient"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/busretry"
|
||||
"git.novox.be/novox/mesh-sdk/go/seatdef"
|
||||
"git.novox.be/novox/mesh-sdk/go/seatlist"
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
)
|
||||
|
||||
@@ -405,6 +408,15 @@ func ownersRead(ask func(key string, body any) (json.RawMessage, error)) view.Re
|
||||
GraphState: indexer("indexer_read_graph")}
|
||||
}
|
||||
|
||||
// holderOf is a seat's holder as mesh-sdk's seatlist.HolderOf answers it (design 47 G A-126): false while the seat has none or
|
||||
// the seat list cannot be read.
|
||||
func holderOf(ask func(key string, body any) (json.RawMessage, error)) func(seat string) (string, bool) {
|
||||
return func(seat string) (string, bool) {
|
||||
module, err := seatlist.HolderOf(ask, seat)
|
||||
return module, err == nil
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
r := ownersRead(stdio.Ask)
|
||||
r.Builds, r.HavesState = read(stdio.State("mesh-delivery-builder.builds")), read(stdio.State("node-engine.have"))
|
||||
@@ -416,16 +428,28 @@ func main() {
|
||||
r.ProvisionsState = read(stdio.State(view.ProvisionsRead))
|
||||
r.BuildLog = stdio.Log(builds.BuildLogRead).Since
|
||||
r.Pending = pendingOf
|
||||
// The build modules are no names (design 47 G A-82): the build seats on the seat list, each held by the module the seat
|
||||
// list names for it; the seat list and its two tools are wired once by mesh-sdk (§204).
|
||||
gate := seatlist.NewPart("view", []string{stdio.AnyPerson, "node-tools", "mesh-healers"})
|
||||
go func() {
|
||||
<-stdio.Served()
|
||||
gate.Follow(seatlist.StdioBus(), nil, func(f string, a ...any) { fmt.Fprintf(os.Stderr, "[mesh-delivery-view-tools] "+f+"\n", a...) })
|
||||
}()
|
||||
seats := buildrun.NewBuildSeats(func() map[string]seatdef.Definition {
|
||||
if rd := gate.Reader(); rd != nil {
|
||||
return rd.Definitions()
|
||||
}
|
||||
return nil
|
||||
}, holderOf(stdio.Ask))
|
||||
u := view.UsageReader{Haves: r.HavesState, Resolved: read(stdio.State("mesh-config.resolved")), Tries: read(stdio.State("mesh-delivery-checker.tries")),
|
||||
Steps: map[string]view.StateRead{}, Pauses: map[string]view.StateRead{}, Backlog: backlogOf(socket())}
|
||||
for _, lang := range buildstep.AskLanguages {
|
||||
module, _ := buildstep.BuildModuleOf(lang)
|
||||
u.Steps[lang] = read(stdio.State(module + "." + view.StepsState))
|
||||
u.Pauses[lang] = read(stdio.State(module + "." + buildstep.PausesState))
|
||||
}
|
||||
Seats: seats,
|
||||
StepsOf: func(seat string) view.StateRead { return read(stdio.State(seat + "." + view.StepsState)) },
|
||||
PausesOf: func(seat string) view.StateRead { return read(stdio.State(seat + "." + buildstep.PausesState)) },
|
||||
Backlog: backlogOf(socket())}
|
||||
// view_first_views and view_first_view_again: where the view's process stands with its first views of the indexer's and
|
||||
// the assigner's states (ADR 0615 §6), and a re-read by hand.
|
||||
tools := append(Tools(r, u), views.StatusTools(viewstate.ViewModule, "view_first_views", "view_first_view_again", stdio.State(views.StatusState), meshhandactsclient.Recorder{Ask: stdio.Ask}, time.Now)...)
|
||||
tools := append(Tools(r, u), gate.Tools()...)
|
||||
tools = append(tools, views.StatusTools(viewstate.ViewModule, "view_first_views", "view_first_view_again", stdio.State(views.StatusState), meshhandactsclient.Recorder{Ask: stdio.Ask}, time.Now)...)
|
||||
if err := stdio.Serve("", tools); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[mesh-delivery-view-tools] %v\n", err)
|
||||
os.Exit(1)
|
||||
|
||||
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"mesh-delivery-view/internal/view"
|
||||
"reflect"
|
||||
@@ -20,6 +21,7 @@ import (
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/busretry"
|
||||
"git.novox.be/novox/mesh-sdk/go/keys"
|
||||
"git.novox.be/novox/mesh-sdk/go/seatlist"
|
||||
)
|
||||
|
||||
type fake map[string]string
|
||||
@@ -625,3 +627,26 @@ func manifestReadsAsksEveryNode(t *testing.T) {
|
||||
t.Errorf("node_manifest_reads = %+v", rows)
|
||||
}
|
||||
}
|
||||
|
||||
// Design 47 G A-82, A-74: a build seat's holder is the module the seat mesh-module-assigner's verb assignments names for it with
|
||||
// the seats, and none while the seat has no holder or the list cannot be read.
|
||||
func TestABuildSeatsHolderIsTheOneTheSeatListNames(t *testing.T) {
|
||||
var asked []string
|
||||
ask := func(key string, _ any) (json.RawMessage, error) {
|
||||
asked = append(asked, key)
|
||||
return json.RawMessage(`{"seats":[{"seat":"node-build-go","scope":"node","holders":[{"module":"acme-go","node":"n1"}]},{"seat":"node-build-node","scope":"node","holders":[]}]}`), nil
|
||||
}
|
||||
holder := holderOf(ask)
|
||||
if m, ok := holder("node-build-go"); !ok || m != "acme-go" {
|
||||
t.Errorf("the holder of node-build-go = %q, %v; want acme-go", m, ok)
|
||||
}
|
||||
if m, ok := holder("node-build-node"); ok {
|
||||
t.Errorf("a seat with no holder has %q", m)
|
||||
}
|
||||
if len(asked) != 2 || asked[0] != seatlist.HolderKey() {
|
||||
t.Errorf("asked %v; want the seat mesh-module-assigner's assignments", asked)
|
||||
}
|
||||
if _, ok := holderOf(func(string, any) (json.RawMessage, error) { return nil, errors.New("down") })("node-build-go"); ok {
|
||||
t.Error("a seat list that cannot be read names a holder")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ require (
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
buildseats "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/buildrun"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
|
||||
)
|
||||
@@ -50,9 +51,11 @@ const (
|
||||
|
||||
// UsageReader reads what build_usage answers from.
|
||||
type UsageReader struct {
|
||||
// Steps and Pauses are each build module's states by language (go, node, image).
|
||||
Steps map[string]StateRead
|
||||
Pauses map[string]StateRead
|
||||
// Seats are the build seats, each by the acting rule that builds, with the module holding it on the seat list (design 47 G
|
||||
// A-82): no build module is named here. StepsOf and PausesOf read a build seat's steps and pauses through the seat.
|
||||
Seats *buildseats.BuildSeats
|
||||
StepsOf func(seat string) StateRead
|
||||
PausesOf func(seat string) StateRead
|
||||
// Resolved is mesh-config's resolved values (<node>.<module>.<key>); Haves the node-engine's have.
|
||||
Resolved StateRead
|
||||
Haves StateRead
|
||||
@@ -209,6 +212,16 @@ func (u UsageReader) Usage() UsageAnswer {
|
||||
note := func(f string, a ...any) { ans.Notes = append(ans.Notes, fmt.Sprintf(f, a...)) }
|
||||
|
||||
tries := u.tries(now, &ans, note)
|
||||
// The build seats once for this answer: each asks the seat list for its holder (design 47 G A-82).
|
||||
buildSeats := u.Seats.All()
|
||||
buildSeatOfModule := func(module string) buildseats.BuildModule {
|
||||
for _, m := range buildSeats {
|
||||
if m.Module == module && module != "" {
|
||||
return m
|
||||
}
|
||||
}
|
||||
return buildseats.BuildModule{}
|
||||
}
|
||||
seats := map[string]map[string]*SeatUsage{} // node -> language -> seat
|
||||
seat := func(node, lang string) *SeatUsage {
|
||||
if seats[node] == nil {
|
||||
@@ -217,9 +230,13 @@ func (u UsageReader) Usage() UsageAnswer {
|
||||
if s := seats[node][lang]; s != nil {
|
||||
return s
|
||||
}
|
||||
name, _ := buildstep.BuildSeat(lang)
|
||||
module, _ := buildstep.BuildModuleOf(lang)
|
||||
s := &SeatUsage{Seat: name, Module: module, Running: RunningUsage{ByKind: map[string]int{}}, LeftRunning: []StepUsage{}, EndedStale: []StepUsage{}, Steps: []StepUsage{}}
|
||||
var bm buildseats.BuildModule
|
||||
for _, m := range buildSeats {
|
||||
if m.AskLanguage() == lang {
|
||||
bm = m
|
||||
}
|
||||
}
|
||||
s := &SeatUsage{Seat: bm.Seat, Module: bm.Module, Running: RunningUsage{ByKind: map[string]int{}}, LeftRunning: []StepUsage{}, EndedStale: []StepUsage{}, Steps: []StepUsage{}}
|
||||
seats[node][lang] = s
|
||||
return s
|
||||
}
|
||||
@@ -233,10 +250,8 @@ func (u UsageReader) Usage() UsageAnswer {
|
||||
} else {
|
||||
var want []string
|
||||
for _, k := range keys {
|
||||
for _, lang := range buildstep.AskLanguages {
|
||||
if m, _ := buildstep.BuildModuleOf(lang); strings.HasSuffix(k, "."+m) {
|
||||
want = append(want, k)
|
||||
}
|
||||
if _, module, _ := strings.Cut(k, "."); buildSeatOfModule(module).Seat != "" {
|
||||
want = append(want, k)
|
||||
}
|
||||
}
|
||||
raws, err := u.Haves.values(want)
|
||||
@@ -256,8 +271,8 @@ func (u UsageReader) Usage() UsageAnswer {
|
||||
}
|
||||
for k, h := range haves {
|
||||
node, module, _ := strings.Cut(k, ".")
|
||||
lang := strings.TrimPrefix(module, "mesh-build-")
|
||||
s := seat(node, lang)
|
||||
bm := buildSeatOfModule(module)
|
||||
s := seat(node, bm.AskLanguage())
|
||||
s.Phase, s.Holds = string(h.Phase), h.Phase == nodeengineclient.PhaseRunning
|
||||
}
|
||||
|
||||
@@ -310,19 +325,27 @@ func (u UsageReader) Usage() UsageAnswer {
|
||||
}
|
||||
for key := range unreadable {
|
||||
node, module, _ := strings.Cut(key, ".")
|
||||
seat(node, strings.TrimPrefix(module, "mesh-build-"))
|
||||
bm := buildSeatOfModule(module)
|
||||
seat(node, bm.AskLanguage())
|
||||
}
|
||||
// Every node with a resolved capacity is listed, so "is this node eligible" is answered for it.
|
||||
for key := range resolved {
|
||||
node, module, _ := strings.Cut(key, ".")
|
||||
seat(node, strings.TrimPrefix(module, "mesh-build-"))
|
||||
bm := buildSeatOfModule(module)
|
||||
seat(node, bm.AskLanguage())
|
||||
}
|
||||
if len(buildSeats) == 0 {
|
||||
note("no build seat is known from the seat definitions and the seat list, so no steps or pauses are read")
|
||||
}
|
||||
// The steps of each build module.
|
||||
steps := map[string]map[string][]usageStep{} // node -> language -> steps
|
||||
for _, lang := range buildstep.AskLanguages {
|
||||
module, _ := buildstep.BuildModuleOf(lang)
|
||||
sr, ok := u.Steps[lang]
|
||||
if !ok || sr.Keys == nil || sr.Get == nil {
|
||||
for _, bm := range buildSeats {
|
||||
lang, module := bm.AskLanguage(), cmpOr(bm.Module, bm.Seat)
|
||||
var sr StateRead
|
||||
if u.StepsOf != nil {
|
||||
sr = u.StepsOf(bm.Seat)
|
||||
}
|
||||
if sr.Keys == nil || sr.Get == nil {
|
||||
note("the steps of %s are not readable here", module)
|
||||
continue
|
||||
}
|
||||
@@ -379,10 +402,13 @@ func (u UsageReader) Usage() UsageAnswer {
|
||||
// Pauses: paused.<node> of each module; and capacity.<node>, the capacity the module says it judges by.
|
||||
paused := map[string]bool{} // <node>.<language>
|
||||
judged := map[string]buildstep.Capacity{}
|
||||
for _, lang := range buildstep.AskLanguages {
|
||||
module, _ := buildstep.BuildModuleOf(lang)
|
||||
pr, ok := u.Pauses[lang]
|
||||
if !ok || pr.Get == nil {
|
||||
for _, bm := range buildSeats {
|
||||
lang, module := bm.AskLanguage(), cmpOr(bm.Module, bm.Seat)
|
||||
var pr StateRead
|
||||
if u.PausesOf != nil {
|
||||
pr = u.PausesOf(bm.Seat)
|
||||
}
|
||||
if pr.Get == nil {
|
||||
note("the pauses of %s are not readable here, so none is said paused", module)
|
||||
continue
|
||||
}
|
||||
@@ -422,7 +448,8 @@ func (u UsageReader) Usage() UsageAnswer {
|
||||
for _, node := range nodes {
|
||||
nu := NodeUsage{Node: node, Seats: []SeatUsage{}, Said: []string{},
|
||||
CPU: "not on the bus: the disk-load module keeps no state; " + node + "/disk-load.cpu_top reads it (the build modules' share among its units)"}
|
||||
for _, lang := range buildstep.AskLanguages {
|
||||
for _, bm := range buildSeats {
|
||||
lang := bm.AskLanguage()
|
||||
s := seats[node][lang]
|
||||
if s == nil {
|
||||
continue
|
||||
@@ -696,3 +723,11 @@ func sumMedian(d []int64) (sum, median int64) {
|
||||
}
|
||||
return sum, s[(len(s)-1)/2]
|
||||
}
|
||||
|
||||
// cmpOr is the first of two names that is not empty.
|
||||
func cmpOr(a, b string) string {
|
||||
if a != "" {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
buildseats "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
||||
"git.novox.be/novox/mesh-sdk/go/seatdef"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -82,8 +84,19 @@ func usageFixture() UsageReader {
|
||||
"shanks.mesh-config.other": `{"value":"x"}`,
|
||||
"g14.mesh-build-node.capacity": `{"node":"g14","module":"mesh-build-node","key":"capacity","source":"unreadable"}`,
|
||||
}),
|
||||
Steps: map[string]StateRead{"go": states(goSteps), "node": states(map[string]string{}), "image": states(map[string]string{})},
|
||||
Pauses: map[string]StateRead{"go": states(map[string]string{"paused.ace": `{"v":1,"paused":true,"by":"lead@shanks","at":"2026-10-26T09:00:00Z"}`}), "node": states(nil), "image": states(nil)},
|
||||
Seats: buildSeatsOf(),
|
||||
StepsOf: func(seat string) StateRead {
|
||||
if seat == "node-build-go" {
|
||||
return states(goSteps)
|
||||
}
|
||||
return states(map[string]string{})
|
||||
},
|
||||
PausesOf: func(seat string) StateRead {
|
||||
if seat == "node-build-go" {
|
||||
return states(map[string]string{"paused.ace": `{"v":1,"paused":true,"by":"lead@shanks","at":"2026-10-26T09:00:00Z"}`})
|
||||
}
|
||||
return states(nil)
|
||||
},
|
||||
Tries: states(map[string]string{
|
||||
"try-" + strconv.FormatInt(at("11:29").UnixNano(), 10): `{"id":"try-1","repository":"novox/mesh-sdk","head":"h","trunk":"t","state":"pass","asks":["` + tryAsk + `"]}`,
|
||||
"try-" + strconv.FormatInt(at("11:58").UnixNano(), 10): `{"id":"try-2","repository":"novox/mesh-delivery","head":"h","trunk":"t","state":"running","started":"2026-10-26T11:58:00Z"}`,
|
||||
@@ -167,7 +180,7 @@ func TestBuildUsageAnswersEveryNodesUseFromTheStates(t *testing.T) {
|
||||
// Nothing readable: each state is named in a note, the backlog's refusal too, and nothing is said held.
|
||||
none := UsageReader{Now: func() time.Time { return usageNow }, Backlog: func() ([]BacklogRow, error) { return nil, errors.New("no socket") }}.Usage()
|
||||
notes := strings.Join(none.Notes, " | ")
|
||||
for _, want := range []string{"have is not readable", "capacity unknown to this tool: mesh-config's resolved values are not readable", "steps of mesh-build-go are not readable", "tries are not readable", "did not answer: no socket"} {
|
||||
for _, want := range []string{"have is not readable", "capacity unknown to this tool: mesh-config's resolved values are not readable", "no build seat is known", "tries are not readable", "did not answer: no socket"} {
|
||||
if !strings.Contains(notes, want) {
|
||||
t.Errorf("the notes %q do not say %q", notes, want)
|
||||
}
|
||||
@@ -196,8 +209,8 @@ func TestBuildUsageAnswersEveryNodesUseFromTheStates(t *testing.T) {
|
||||
// Issue 1806's follow-up: the module on shanks says it judges by 1 of mesh-config's 2 (its build CPU bounds it); the
|
||||
// seat says the module's capacity and why. And issue 1890: a running try that names its asks is said with them.
|
||||
u = usageFixture()
|
||||
u.Pauses["go"] = states(map[string]string{"capacity.shanks": `{"v":1,"effective":1,"configured":2,"quota-from":"the cpu.max of /meshyield.slice","quota-cores":6,"cores-per-step":6,"by-cpu":1,"why":"1, bounded by the CPU","at":"2026-10-26T11:00:00Z"}`,
|
||||
"capacity.ace": `{"v":1,"effective":2,"configured":3,"why":"stale: mesh-config says 2 now"}`})
|
||||
setPauses(&u, "node-build-go", states(map[string]string{"capacity.shanks": `{"v":1,"effective":1,"configured":2,"quota-from":"the cpu.max of /meshyield.slice","quota-cores":6,"cores-per-step":6,"by-cpu":1,"why":"1, bounded by the CPU","at":"2026-10-26T11:00:00Z"}`,
|
||||
"capacity.ace": `{"v":1,"effective":2,"configured":3,"why":"stale: mesh-config says 2 now"}`}))
|
||||
u.Tries = states(map[string]string{"try-" + strconv.FormatInt(at("11:58").UnixNano(), 10): `{"id":"try-2","repository":"novox/mesh-delivery","head":"h","trunk":"t","state":"running","asks":["` + tryAsk0 + `"],"steps":[{"ask":"` + tryAsk0 + `","node":"shanks","state":"running","phase":"unit: tests of 3 packages"}]}`})
|
||||
bound := u.Usage()
|
||||
sc := seatOf(t, bound, "shanks", "node-build-go")
|
||||
@@ -208,13 +221,13 @@ func TestBuildUsageAnswersEveryNodesUseFromTheStates(t *testing.T) {
|
||||
t.Errorf("a module's word older than mesh-config's value is not taken: %+v", sa)
|
||||
}
|
||||
// The operator, 2026-10-26: outside its build hours the seat takes nothing new, and says so.
|
||||
u.Pauses["go"] = states(map[string]string{"capacity.shanks": `{"v":1,"effective":0,"configured":2,"build-hours":"18:00-06:00 CET","outside-hours":true,"why":"0, outside its build hours 18:00-06:00 CET until 18:00 (otherwise 2)","at":"2026-10-26T11:00:00Z"}`})
|
||||
setPauses(&u, "node-build-go", states(map[string]string{"capacity.shanks": `{"v":1,"effective":0,"configured":2,"build-hours":"18:00-06:00 CET","outside-hours":true,"why":"0, outside its build hours 18:00-06:00 CET until 18:00 (otherwise 2)","at":"2026-10-26T11:00:00Z"}`}))
|
||||
if so := seatOf(t, u.Usage(), "shanks", "node-build-go"); so.Capacity != 0 || !so.OutsideHours || so.Eligible == nil || *so.Eligible ||
|
||||
so.WhyNot != "outside its build hours 18:00-06:00 CET on shanks: it takes nothing new until they open" {
|
||||
t.Errorf("a seat outside its build hours: %+v", so)
|
||||
}
|
||||
// A build-hours value the module cannot read: it builds at any hour, and the seat says the value and why.
|
||||
u.Pauses["go"] = states(map[string]string{"capacity.shanks": `{"v":1,"effective":2,"configured":2,"build-hours-fault":"the build hours \"shanks=18:00-6\": \"6\" is not a time HH:MM","why":"2; at any hour, because its build hours cannot be read","at":"2026-10-26T11:00:00Z"}`})
|
||||
setPauses(&u, "node-build-go", states(map[string]string{"capacity.shanks": `{"v":1,"effective":2,"configured":2,"build-hours-fault":"the build hours \"shanks=18:00-6\": \"6\" is not a time HH:MM","why":"2; at any hour, because its build hours cannot be read","at":"2026-10-26T11:00:00Z"}`}))
|
||||
if sf := seatOf(t, u.Usage(), "shanks", "node-build-go"); sf.BuildHours != "any hour" || !strings.Contains(sf.BuildHoursFault, "shanks=18:00-6") || sf.Eligible == nil || !*sf.Eligible {
|
||||
t.Errorf("a seat whose build hours cannot be read: %+v", sf)
|
||||
}
|
||||
@@ -225,3 +238,25 @@ func TestBuildUsageAnswersEveryNodesUseFromTheStates(t *testing.T) {
|
||||
}
|
||||
|
||||
var tryAsk0 = stepID(at("11:58"), "a00010")
|
||||
|
||||
// buildSeatsOf is the build seats as the seat definitions and the seat list give them (design 47 G A-82): the seats whose
|
||||
// acting rule builds, each held by the module the seat list names.
|
||||
func buildSeatsOf() *buildseats.BuildSeats {
|
||||
rule := func(on string) seatdef.Definition {
|
||||
return seatdef.Definition{Acts: []seatdef.ActingRule{{Step: seatdef.StepBuild, On: on}}}
|
||||
}
|
||||
defs := map[string]seatdef.Definition{"node-build-go": rule("go"), "node-build-node": rule("typescript"), "node-build-image": rule(seatdef.OnKindImage)}
|
||||
holders := map[string]string{"node-build-go": "mesh-build-go", "node-build-node": "mesh-build-node", "node-build-image": "mesh-build-image"}
|
||||
return buildseats.NewBuildSeats(func() map[string]seatdef.Definition { return defs }, func(seat string) (string, bool) { h, ok := holders[seat]; return h, ok })
|
||||
}
|
||||
|
||||
// setPauses replaces what one build seat's pauses read.
|
||||
func setPauses(u *UsageReader, seat string, st StateRead) {
|
||||
prev := u.PausesOf
|
||||
u.PausesOf = func(s string) StateRead {
|
||||
if s == seat {
|
||||
return st
|
||||
}
|
||||
return prev(s)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ type manifest struct {
|
||||
OwnSecrets map[string]string `json:"own-secrets"`
|
||||
SecretsOwner string `json:"secrets-owner"`
|
||||
Resources []map[string]any `json:"resources"`
|
||||
Requires []require `json:"requires"`
|
||||
Items []item `json:"items"`
|
||||
Build struct {
|
||||
Artifacts []artifact `json:"artifacts"`
|
||||
@@ -105,7 +106,7 @@ func TestManifestIsTakenAndReadsOnly(t *testing.T) { // 1134/AC7
|
||||
if m.Module != "mesh-delivery-view" {
|
||||
t.Errorf("module = %q", m.Module)
|
||||
}
|
||||
want := []string{"mesh-build-go.pauses", "mesh-build-go.steps", "mesh-build-image.pauses", "mesh-build-image.steps", "mesh-build-node.pauses", "mesh-build-node.steps", "mesh-conditions.conditions", "mesh-config.resolved", "mesh-delivery-builder.build-log", "mesh-delivery-builder.builds", "mesh-delivery-checker.tries", "mesh-delivery-gate.asks", "mesh-delivery-gate.source", "mesh-delivery-teller.answers", "mesh-delivery-watchman.presence", "mesh-delivery-watchman.stuck", "mesh-healers.attempts", "mesh-provisioner.provisions", "mesh-vault.signers", "node-engine.have"} // no reads of the indexer's or the assigner's states (ADR 0615 §1, DLV-M4); view_tiers reads the module graph and the provisioner's keys (ADR 0603 §6); view_status reads the keeper's conditions and the healers' attempts (1952/V1); build_usage reads the build modules' steps and pauses, mesh-config's capacities and the checker's tries; 1168/AC1, 1241/AC4, 1345/AC3, 1297/B8, B9
|
||||
want := []string{"mesh-conditions.conditions", "mesh-config.resolved", "mesh-delivery-builder.build-log", "mesh-delivery-builder.builds", "mesh-delivery-checker.tries", "mesh-delivery-gate.asks", "mesh-delivery-gate.source", "mesh-delivery-teller.answers", "mesh-delivery-watchman.presence", "mesh-delivery-watchman.stuck", "mesh-healers.attempts", "mesh-provisioner.provisions", "mesh-vault.signers", "node-build-go.pauses", "node-build-go.steps", "node-build-image.pauses", "node-build-image.steps", "node-build-node.pauses", "node-build-node.steps", "node-engine.have"} // the build seats' states by seat, never a build module (design 47 G A-82); // no reads of the indexer's or the assigner's states (ADR 0615 §1, DLV-M4); view_tiers reads the module graph and the provisioner's keys (ADR 0603 §6); view_status reads the keeper's conditions and the healers' attempts (1952/V1); build_usage reads the build modules' steps and pauses, mesh-config's capacities and the checker's tries; 1168/AC1, 1241/AC4, 1345/AC3, 1297/B8, B9
|
||||
got := slices.Clone(m.Reads)
|
||||
slices.Sort(got)
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
@@ -123,19 +124,24 @@ func TestManifestIsTakenAndReadsOnly(t *testing.T) { // 1134/AC7
|
||||
"mesh-delivery-indexer.indexer_read_graph", "mesh-delivery-indexer.indexer_read_heard", "mesh-delivery-indexer.indexer_pending",
|
||||
"mesh-delivery-indexer.indexer_read_packages", "mesh-delivery-indexer.indexer_read_predefined", "mesh-delivery-indexer.indexer_read_taken",
|
||||
"seat:mesh-catalog.revision", // ADR 0663: the modules are the catalog half's, read through the seat's verb revision, never the indexer's
|
||||
"seat:mesh-catalog.seat-definitions", // design 47 G A-82, §204: the build seats are read from the seat list
|
||||
"seat:mesh-hand-acts.hand-act", // the join: its first view asked again by hand is a hand act (views.StatusTools)
|
||||
"seat:mesh-module-assigner.assignments", // A-82: the holder of each build seat
|
||||
"seat:node-engine.manifest-reads"}; !reflect.DeepEqual(m.Invokes, want) { // ADR 0618 §3: node_manifest_reads asks the node-engine seat's verb of every node; 1585/T24: view_tiers asks indexer_pending, a verb edge (ADR 0614 §2)
|
||||
t.Errorf("invokes = %v, want %v", m.Invokes, want)
|
||||
}
|
||||
// design 47 G §387: the keeper's ask for the findings is heard, the finding stream declared (§383), and §368: version is MAJOR.MINOR.
|
||||
if want := []stream{{Name: "mesh-delivery-view-findings", Events: []string{"finding-written.*", "finding-cleared.*", "findings-stated.*"}, MaxAgeDays: 7, MaxMiB: 16}}; !reflect.DeepEqual(m.Streams, want) {
|
||||
t.Errorf("streams = %+v, want %+v", m.Streams, want)
|
||||
if len(m.Streams) != 0 {
|
||||
t.Errorf("streams = %+v: the finding stream is a require now", m.Streams)
|
||||
}
|
||||
if want := findingRequires(1); !reflect.DeepEqual(m.Requires, want) {
|
||||
t.Errorf("requires = %+v, want %+v", m.Requires, want)
|
||||
}
|
||||
if m.Version != "0.1" {
|
||||
t.Errorf("version = %q, want 0.1", m.Version)
|
||||
}
|
||||
if want := []string{"mesh-conditions.findings-asked", "mesh-delivery-assigner.views.asks", "mesh-delivery-assigner.views.assignments", "mesh-delivery-assigner.views.wants",
|
||||
"mesh-catalog.catalog-revision", "mesh-delivery-indexer.views.candidates", "mesh-delivery-indexer.views.taken"}; !reflect.DeepEqual(m.Consumes, want) {
|
||||
"mesh-catalog.catalog-revision", "mesh-catalog.seat-definition-changed", "mesh-catalog.seat-definition-removed", "mesh-delivery-indexer.views.candidates", "mesh-delivery-indexer.views.taken"}; !reflect.DeepEqual(m.Consumes, want) {
|
||||
t.Errorf("consumes = %v, want %v", m.Consumes, want)
|
||||
}
|
||||
// The one event the view says: a source revision is delivered (1906/D9, ADR 0539 §17), derived by it from the wants and the haves.
|
||||
@@ -149,7 +155,7 @@ func TestManifestIsTakenAndReadsOnly(t *testing.T) { // 1134/AC7
|
||||
// The states the view writes: the operator's answers to the asks that need no proof (ADR 0528 clause 6, 1297/B9), which the
|
||||
// teller reads, and how far each repository reached (ADR 0556 clause 2, issue 1280), which the gate reads.
|
||||
// Its first views of the owners' states and the finding of one unanswered past its bound (ADR 0615 §6).
|
||||
if want := []state{{Name: "answers", History: 5}, {Name: "reach", History: 1}, {Name: "first-views", History: 1}, {Name: "findings", History: 1}}; !reflect.DeepEqual(m.State, want) {
|
||||
if want := []state{{Name: "answers", History: 5}, {Name: "reach", History: 1}, {Name: "first-views", History: 1}}; !reflect.DeepEqual(m.State, want) {
|
||||
t.Errorf("state = %+v, want %+v", m.State, want)
|
||||
}
|
||||
if _, ok := m.OwnSecrets["broker"]; !ok || len(m.OwnSecrets) != 1 {
|
||||
@@ -160,7 +166,7 @@ func TestManifestIsTakenAndReadsOnly(t *testing.T) { // 1134/AC7
|
||||
}
|
||||
// The two read tools of the indexer's modules (issue 1562), carried by a bundle the runtime launches; the indexer
|
||||
// itself serves no tool (ADR 0437 §4). Carrying them gives the tool runner read access to every bucket the view reads.
|
||||
if want := []string{"view_status", "indexer_module", "indexer_modules", "indexer_candidate", "indexer_candidates", "indexer_predefined", "indexer_config_candidates", "indexer_packages", "builder_build", "builder_builds", "builder_history", "assigner_wants", "assigner_want", "assigner_differences", "node_haves", "node_trusts", "build_usage", "view_reach", "indexer_module_history", "indexer_heard", "view_tiers", "view_first_views", "view_first_view_again", "node_manifest_reads"}; !reflect.DeepEqual(m.Tools, want) {
|
||||
if want := []string{"view_status", "indexer_module", "indexer_modules", "indexer_candidate", "indexer_candidates", "indexer_predefined", "indexer_config_candidates", "indexer_packages", "builder_build", "builder_builds", "builder_history", "assigner_wants", "assigner_want", "assigner_differences", "node_haves", "node_trusts", "build_usage", "view_reach", "indexer_module_history", "indexer_heard", "view_tiers", "view_first_views", "view_first_view_again", "node_manifest_reads", "view_seat_list", "view_seat_list_again"}; !reflect.DeepEqual(m.Tools, want) {
|
||||
t.Errorf("tools = %v, want %v", m.Tools, want)
|
||||
}
|
||||
// D-11: the holders and buckets the view names are its manifest's (no contract names a holder, novox/hq ADR 0634 §1): a
|
||||
@@ -430,3 +436,22 @@ func TestEveryStateTheCommandsReadIsIssued(t *testing.T) {
|
||||
t.Fatal("no stdio.State literal found under cmd/: the scan reads nothing")
|
||||
}
|
||||
}
|
||||
|
||||
// require is one entry of the manifest's requires: a provision the module asks for by a template (ADR 0595), which the
|
||||
// controller's reader takes.
|
||||
type require struct {
|
||||
Name string `json:"name"`
|
||||
Storage string `json:"storage"`
|
||||
Template string `json:"template"`
|
||||
Options map[string]any `json:"options"`
|
||||
}
|
||||
|
||||
// findingRequires is the findings state and the finding stream a module asks for by their templates (design 47 G
|
||||
// §383, §387; A-30): 7 days and 16 MiB, the findings state keeping the given history.
|
||||
func findingRequires(history float64) []require {
|
||||
return []require{
|
||||
{Name: "findings", Storage: "key-value", Template: "state", Options: map[string]any{"history": history}},
|
||||
{Name: "finding-events", Storage: "stream", Template: "events", Options: map[string]any{
|
||||
"events": []any{"finding-written.*", "finding-cleared.*", "findings-stated.*"}, "max-age-days": float64(7), "max-mib": float64(16)}},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,9 @@
|
||||
"view_tiers",
|
||||
"view_first_views",
|
||||
"view_first_view_again",
|
||||
"node_manifest_reads"
|
||||
"node_manifest_reads",
|
||||
"view_seat_list",
|
||||
"view_seat_list_again"
|
||||
],
|
||||
"emits": [
|
||||
"delivered",
|
||||
@@ -56,6 +58,8 @@
|
||||
"mesh-delivery-assigner.views.assignments",
|
||||
"mesh-delivery-assigner.views.wants",
|
||||
"mesh-catalog.catalog-revision",
|
||||
"mesh-catalog.seat-definition-changed",
|
||||
"mesh-catalog.seat-definition-removed",
|
||||
"mesh-delivery-indexer.views.candidates",
|
||||
"mesh-delivery-indexer.views.taken"
|
||||
],
|
||||
@@ -73,16 +77,18 @@
|
||||
"mesh-delivery-indexer.indexer_read_predefined",
|
||||
"mesh-delivery-indexer.indexer_read_taken",
|
||||
"seat:mesh-catalog.revision",
|
||||
"seat:mesh-catalog.seat-definitions",
|
||||
"seat:mesh-hand-acts.hand-act",
|
||||
"seat:mesh-module-assigner.assignments",
|
||||
"seat:node-engine.manifest-reads"
|
||||
],
|
||||
"reads": [
|
||||
"mesh-build-go.pauses",
|
||||
"mesh-build-go.steps",
|
||||
"mesh-build-image.pauses",
|
||||
"mesh-build-image.steps",
|
||||
"mesh-build-node.pauses",
|
||||
"mesh-build-node.steps",
|
||||
"node-build-go.pauses",
|
||||
"node-build-go.steps",
|
||||
"node-build-image.pauses",
|
||||
"node-build-image.steps",
|
||||
"node-build-node.pauses",
|
||||
"node-build-node.steps",
|
||||
"mesh-config.resolved",
|
||||
"mesh-delivery-builder.build-log",
|
||||
"mesh-delivery-builder.builds",
|
||||
|
||||
@@ -69,6 +69,7 @@ type manifest struct {
|
||||
OwnSecrets map[string]string `json:"own-secrets"`
|
||||
SecretsOwner string `json:"secrets-owner"`
|
||||
Resources []map[string]any `json:"resources"`
|
||||
Requires []require `json:"requires"`
|
||||
Build struct {
|
||||
Artifacts []artifact `json:"artifacts"`
|
||||
} `json:"build"`
|
||||
@@ -98,7 +99,7 @@ func TestManifestStateAndReads(t *testing.T) { // 1135/AC6
|
||||
if m.Module != "mesh-delivery-watchman" || m.Moved || m.Slug != "watchman" {
|
||||
t.Errorf("module %q moved %v (it has no template, so it is not moved) slug %q", m.Module, m.Moved, m.Slug)
|
||||
}
|
||||
want := []state{{Name: "presence", History: 20}, {Name: "pulse", KeyTTL: true}, {Name: "stuck", History: 20}, {Name: "findings", History: 5}, {Name: "first-views", History: 1}}
|
||||
want := []state{{Name: "presence", History: 20}, {Name: "pulse", KeyTTL: true}, {Name: "stuck", History: 20}, {Name: "first-views", History: 1}}
|
||||
if !reflect.DeepEqual(m.State, want) {
|
||||
t.Errorf("state = %+v, want %+v", m.State, want)
|
||||
}
|
||||
@@ -114,9 +115,11 @@ func TestManifestStateAndReads(t *testing.T) { // 1135/AC6
|
||||
t.Errorf("version = %q, want MAJOR.MINOR", m.Version)
|
||||
}
|
||||
// Design 47 G §387: the finding stream, 7 days and 16 MiB, and the keeper's ask.
|
||||
if len(m.Streams) != 1 || m.Streams[0].Name != "mesh-delivery-watchman-findings" || m.Streams[0].MaxAgeDays != 7 || m.Streams[0].MaxMiB != 16 ||
|
||||
!reflect.DeepEqual(m.Streams[0].Events, []string{"finding-written.*", "finding-cleared.*", "findings-stated.*"}) {
|
||||
t.Errorf("streams = %+v, want the finding stream", m.Streams)
|
||||
if len(m.Streams) != 0 {
|
||||
t.Errorf("streams = %+v: the finding stream is a require now", m.Streams)
|
||||
}
|
||||
if want := findingRequires(5); !reflect.DeepEqual(m.Requires, want) {
|
||||
t.Errorf("requires = %+v, want %+v", m.Requires, want)
|
||||
}
|
||||
// DLV-M5 (ADR 0615 §1): the assigner's wants and the indexer's modules are their owners' states, asked by their read
|
||||
// tools and followed by their views families, never read.
|
||||
@@ -272,3 +275,22 @@ func TestNoTickerTimerOrSleep(t *testing.T) { // 1135/AC7
|
||||
t.Fatalf("read %d source files, want the program's", files)
|
||||
}
|
||||
}
|
||||
|
||||
// require is one entry of the manifest's requires: a provision the module asks for by a template (ADR 0595), which the
|
||||
// controller's reader takes.
|
||||
type require struct {
|
||||
Name string `json:"name"`
|
||||
Storage string `json:"storage"`
|
||||
Template string `json:"template"`
|
||||
Options map[string]any `json:"options"`
|
||||
}
|
||||
|
||||
// findingRequires is the findings state and the finding stream a module asks for by their templates (design 47 G
|
||||
// §383, §387; A-30): 7 days and 16 MiB, the findings state keeping the given history.
|
||||
func findingRequires(history float64) []require {
|
||||
return []require{
|
||||
{Name: "findings", Storage: "key-value", Template: "state", Options: map[string]any{"history": history}},
|
||||
{Name: "finding-events", Storage: "stream", Template: "events", Options: map[string]any{
|
||||
"events": []any{"finding-written.*", "finding-cleared.*", "findings-stated.*"}, "max-age-days": float64(7), "max-mib": float64(16)}},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user