Builder runner reads a node's toolchains through the Delivery SDK's neutral toolchain value and key (B2g), which gains the key's parse (ADR 0634 §1, §6; issue 1966)

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-05 16:19:55 +01:00
parent 989d41f3cc
commit 0f5f8bf4c8
7 changed files with 46 additions and 22 deletions
@@ -5,7 +5,8 @@ package buildrun
// names a holder (novox/hq ADR 0625 §4, ADR 0634 §1).
const (
// PlacementBucket is the assigner's state placement, which the toolchain gate reads for the placing of a build module.
PlacementBucket = "mesh-delivery-assigner_placement"
AssignerModule = "mesh-delivery-assigner"
PlacementBucket = AssignerModule + "_placement"
)
// StepsBucket is the bucket of a build module's state steps.
@@ -13,7 +13,7 @@ import (
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/nodebuildgo"
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
"git.novox.be/novox/mesh-sdk/go/process"
)
@@ -136,7 +136,7 @@ func (g *ToolchainGate) again(node string, modules []string) bool {
continue
}
for _, k := range slices.Sorted(maps.Keys(g.raw[module])) {
if n, _, ok := nodebuildgo.ParseToolchainsByNodeLanguageKey(k); ok && n == node {
if n, _, err := buildstep.ParseToolchainKey(k); err == nil && n == node {
changed = g.pass(module, k) || changed
}
}
@@ -151,9 +151,9 @@ func (g *ToolchainGate) pass(module, key string) bool {
if !held {
return g.forward(module, key, nil, true)
}
node, _, ok := nodebuildgo.ParseToolchainsByNodeLanguageKey(key)
node, _, err := buildstep.ParseToolchainKey(key)
why := ""
if ok && g.known && g.placed.Seeded() {
if err == nil && g.known && g.placed.Seeded() {
why = g.leftOut(module, node, value)
}
if why == "" {
@@ -175,8 +175,8 @@ func (g *ToolchainGate) leftOut(module, node string, value []byte) string {
if !placed {
return fmt.Sprintf("the catalogue does not place %s on %s, so it is no builder", module, node)
}
tc, err := nodebuildgo.DecodeToolchainsByNodeLanguageValue(value)
if err == nil && tc.At != nil && tc.At.Before(since) {
tc, err := buildstep.DecodeToolchain(value)
if err == nil && tc.At.Before(since) {
return fmt.Sprintf("it was written at %s, before %s was placed on %s at %s: it counts once %s writes it again",
tc.At.UTC().Format(time.RFC3339), module, node, since.UTC().Format(time.RFC3339), node)
}
@@ -3,7 +3,6 @@ package buildrun
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"log"
@@ -15,14 +14,13 @@ import (
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/nodebuildgo"
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
)
func gateReport(t *testing.T, node, version string, at time.Time) (string, []byte) {
t.Helper()
key := nodebuildgo.ToolchainsByNodeLanguageKey(node, "typescript")
value, err := json.Marshal(nodebuildgo.Toolchain{Language: "typescript", Version: version, Recipe: "f59571cf4be75adf", At: &at})
key, _ := buildstep.ToolchainKey(node, "typescript")
value, err := buildstep.EncodeToolchain(buildstep.Toolchain{Language: "typescript", Version: version, Recipe: "f59571cf4be75adf", At: at})
if err != nil {
t.Fatal(err)
}
@@ -3,7 +3,6 @@ package buildrun
import (
"sync"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/nodebuildgo"
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
)
@@ -14,7 +13,7 @@ var LanguageOf = map[string]string{GoBuildModule: "go", NodeBuildModule: "typesc
// put them in. Safe for use from the watches and from a build at once.
type ToolchainSet struct {
mu sync.Mutex
by map[string]map[string]nodebuildgo.ToolchainsByNodeLanguageValue // language -> node -> entry
by map[string]map[string]buildstep.Toolchain // language -> node -> entry
}
// Put takes a change of a build module's toolchains bucket in: a put of a node's entry (key <node>.<language>) or its
@@ -25,8 +24,8 @@ func (s *ToolchainSet) Put(module, key string, value []byte, deleted bool) bool
if !ok {
return false
}
node, keyLanguage, ok := nodebuildgo.ParseToolchainsByNodeLanguageKey(key)
if !ok || keyLanguage != language {
node, keyLanguage, err := buildstep.ParseToolchainKey(key)
if err != nil || keyLanguage != language {
return false
}
s.mu.Lock()
@@ -38,15 +37,15 @@ func (s *ToolchainSet) Put(module, key string, value []byte, deleted bool) bool
delete(s.by[language], node)
return true
}
tc, err := nodebuildgo.DecodeToolchainsByNodeLanguageValue(value)
tc, err := buildstep.DecodeToolchain(value)
if err != nil || tc.Language != language {
return false
}
if s.by == nil {
s.by = map[string]map[string]nodebuildgo.ToolchainsByNodeLanguageValue{}
s.by = map[string]map[string]buildstep.Toolchain{}
}
if s.by[language] == nil {
s.by[language] = map[string]nodebuildgo.ToolchainsByNodeLanguageValue{}
s.by[language] = map[string]buildstep.Toolchain{}
}
cur, had := s.by[language][node]
s.by[language][node] = tc
@@ -59,7 +58,7 @@ func (s *ToolchainSet) Entries(language string) []buildstep.NodeToolchain {
defer s.mu.Unlock()
var out []buildstep.NodeToolchain
for node, tc := range s.by[language] {
out = append(out, buildstep.NodeToolchain{Node: node, Toolchain: buildstep.Toolchain{Language: tc.Language, Version: tc.Version, Recipe: tc.Recipe}})
out = append(out, buildstep.NodeToolchain{Node: node, Toolchain: tc})
}
return out
}
@@ -1,17 +1,16 @@
package buildrun
import (
"encoding/json"
"testing"
"time"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/nodebuildgo"
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
)
func tcValue(t *testing.T, language, version string) []byte {
t.Helper()
now := time.Now()
b, err := json.Marshal(nodebuildgo.Toolchain{Language: language, Version: version, Recipe: "1", At: &now})
b, err := buildstep.EncodeToolchain(buildstep.Toolchain{Language: language, Version: version, Recipe: "1", At: now})
if err != nil {
t.Fatal(err)
}
+12
View File
@@ -77,3 +77,15 @@ func AgreedToolchain(entries []NodeToolchain) (tc Toolchain, none string) {
a := ToolchainAgreement(entries)
return a.Toolchain, a.Wait
}
// ParseToolchainKey is the inverse of ToolchainKey.
func ParseToolchainKey(key string) (node, language string, err error) {
node, language, ok := strings.Cut(key, ".")
if !ok {
return "", "", fmt.Errorf("buildstep: %q is not a toolchain key, <node>.<language>", key)
}
if _, err := ToolchainKey(node, language); err != nil {
return "", "", fmt.Errorf("buildstep: %q is not a toolchain key: %w", key, err)
}
return node, language, nil
}
+15
View File
@@ -56,3 +56,18 @@ func TestToolchainAgreement(t *testing.T) {
}
}
}
func TestToolchainKeyParsesBack(t *testing.T) {
key, err := ToolchainKey("ace", "go")
if err != nil {
t.Fatal(err)
}
if node, lang, err := ParseToolchainKey(key); err != nil || node != "ace" || lang != "go" {
t.Errorf("%q parsed to %q %q %v", key, node, lang, err)
}
for _, bad := range []string{"ace", "ace.rust", ".go", "a.b.go"} {
if _, _, err := ParseToolchainKey(bad); err == nil {
t.Errorf("%q parsed", bad)
}
}
}