From a5bb2ba5b5a003d86fc2fd4089e3eabdc4a3be29 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 12 Nov 2026 01:30:37 +0100 Subject: [PATCH] fix9b2-2: the builder's process answers the seat mesh-builds' verb reserved; the tools bundle no longer serves it serves beside the process in the manifest (no bus), the catalog half's carriers and seat mesh-catalog.versions used in the process; builder_build stays the bundle's. Builds: design 47 G A-115, A-127, A-87 --- .../cmd/mesh-delivery-builder-tools/main.go | 15 +---- .../mesh-delivery-builder-tools/reserved.go | 21 +------ .../reservedname_test.go | 30 +++------- .../mesh-delivery-builder/integration_test.go | 11 ++++ .../cmd/mesh-delivery-builder/main.go | 8 +++ .../cmd/mesh-delivery-builder/reservedverb.go | 60 +++++++++++++++++++ .../reservedverb_test.go | 45 ++++++++++++++ .../mesh-delivery-builder/manifest_test.go | 15 ++++- modules/mesh-delivery-builder/module.json | 4 +- 9 files changed, 153 insertions(+), 56 deletions(-) create mode 100644 modules/mesh-delivery-builder/cmd/mesh-delivery-builder/reservedverb.go create mode 100644 modules/mesh-delivery-builder/cmd/mesh-delivery-builder/reservedverb_test.go diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/main.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/main.go index f2db1a07..2ba23cde 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/main.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/main.go @@ -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) diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/reserved.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/reserved.go index d6a45807..983a54f4 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/reserved.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/reserved.go @@ -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"}, diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/reservedname_test.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/reservedname_test.go index e23c2dba..26b51535 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/reservedname_test.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/reservedname_test.go @@ -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) { diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/integration_test.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/integration_test.go index 73977984..fd9cd17c 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/integration_test.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/integration_test.go @@ -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" @@ -556,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) diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go index de27d334..59e3138e 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go @@ -403,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 } diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/reservedverb.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/reservedverb.go new file mode 100644 index 00000000..49bd2b14 --- /dev/null +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/reservedverb.go @@ -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 + } +} diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/reservedverb_test.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/reservedverb_test.go new file mode 100644 index 00000000..ef87f65e --- /dev/null +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/reservedverb_test.go @@ -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") + } +} diff --git a/modules/mesh-delivery-builder/manifest_test.go b/modules/mesh-delivery-builder/manifest_test.go index 976dfedd..1311e4e5 100644 --- a/modules/mesh-delivery-builder/manifest_test.go +++ b/modules/mesh-delivery-builder/manifest_test.go @@ -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) diff --git a/modules/mesh-delivery-builder/module.json b/modules/mesh-delivery-builder/module.json index 0c5ad37b..74a51154 100644 --- a/modules/mesh-delivery-builder/module.json +++ b/modules/mesh-delivery-builder/module.json @@ -35,7 +35,6 @@ "builder_toolchains_drop", "builder_waits", "builder_versions", - "reserved", "builder_first_views", "builder_first_view_again" ], @@ -208,6 +207,9 @@ "type": "process", "name": "mesh-delivery-builder", "artifact": "program", + "serves": [ + "seat:mesh-builds.reserved" + ], "run": [ "./mesh-delivery-builder" ],