The builder's asker on the seat operator-channel (level approve, about the key, state asked), the teller's three bus adapters (publish, the router's record through the seat's holder on the seat list, decided heard through the hub's events handler); the manifest uses the seat and consumes decided. Builds: design 47 G §568, A-74 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
131 lines
5.4 KiB
Go
131 lines
5.4 KiB
Go
package build
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sync"
|
|
"testing"
|
|
|
|
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
|
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
|
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
|
|
)
|
|
|
|
type actedStore struct {
|
|
mu sync.Mutex
|
|
vals map[string][]byte
|
|
revs map[string]uint64
|
|
}
|
|
|
|
func (s *actedStore) Get(_ context.Context, k string) ([]byte, uint64, bool, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
v, ok := s.vals[k]
|
|
return v, s.revs[k], ok, nil
|
|
}
|
|
func (s *actedStore) Create(_ context.Context, k string, v []byte) (uint64, error) {
|
|
return s.Update(context.Background(), k, v, 0)
|
|
}
|
|
func (s *actedStore) Update(_ context.Context, k string, v []byte, _ uint64) (uint64, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if s.vals == nil {
|
|
s.vals, s.revs = map[string][]byte{}, map[string]uint64{}
|
|
}
|
|
s.vals[k], s.revs[k] = v, s.revs[k]+1
|
|
return s.revs[k], nil
|
|
}
|
|
func (s *actedStore) KeysOf(context.Context, string) ([]string, error) { return nil, nil }
|
|
func (s *actedStore) Delete(_ context.Context, k string) error { delete(s.vals, k); return nil }
|
|
|
|
// Design 47 G §568, ADR 0673: a key failed on a transport fault is built again on the node-engine seat's events that say its
|
|
// cause passed, once per event, counted in event-builds; after three the next event asks the operator once.
|
|
func TestRetakeOnAnEvent(t *testing.T) {
|
|
r := newRig(t, goBundle)
|
|
// The first look creates the key and its note meets the fault; a retake's claim is an update that succeeds and the note after
|
|
// it is the second.
|
|
fault := fmt.Errorf("the key: %w", context.DeadlineExceeded)
|
|
failOnce := func() { r.st.before = func() { r.st.before = func() { r.st.faults = []error{fault} } } }
|
|
r.st.faults = []error{fault}
|
|
if v := r.consider(t, "modules/m"); !builds.FailedOnTransportFault(v.Error) {
|
|
t.Fatalf("the key = %+v, want failed on a transport fault", v)
|
|
}
|
|
var asked []string
|
|
rt := &Retaker{Builder: r.b, Acted: &actedStore{},
|
|
Entry: func(context.Context, string) (meshcatalog.CatalogEntry, error) {
|
|
return meshcatalog.CatalogEntry{Serves: []string{"node-build-go.steps"}}, nil
|
|
},
|
|
Ask: func(_ context.Context, key string, _ builds.BuilderBuild, _ uint64) error {
|
|
asked = append(asked, key)
|
|
return nil
|
|
}}
|
|
running, _ := json.Marshal(nodeengine.Running{Module: "mesh-build-go", Node: "g14", Fingerprint: "f1"})
|
|
started, _ := json.Marshal(nodeengine.ManifestReadsChanged{Node: "ace", Why: "started"})
|
|
heard := func(seq uint64, subject string, data []byte) {
|
|
t.Helper()
|
|
if ok, err := rt.Heard(context.Background(), subject, data, seq); !ok || err != nil {
|
|
t.Fatalf("heard %s: %v, %v", subject, ok, err)
|
|
}
|
|
r.b.Wait()
|
|
}
|
|
runningSubject := "mesh.seat.node-engine.event.running.g14.mesh-build-go"
|
|
readsSubject := "mesh.seat.node-engine.event.manifest-reads-changed.ace"
|
|
// A `started` builds nothing, and the same message again builds nothing.
|
|
heard(1, readsSubject, started)
|
|
if v := valueOf(t, r.st, r.key); v.EventBuilds != 0 || v.State != builds.BuildFailed {
|
|
t.Fatalf("a started: the key = %+v", v)
|
|
}
|
|
for i, ev := range []struct {
|
|
seq uint64
|
|
subject string
|
|
data []byte
|
|
}{{2, runningSubject, running}, {3, runningSubject + ".2", running}, {4, runningSubject + ".3", running}} {
|
|
failOnce()
|
|
heard(ev.seq, ev.subject, ev.data)
|
|
if v := valueOf(t, r.st, r.key); v.EventBuilds != i+1 || !builds.FailedOnTransportFault(v.Error) {
|
|
t.Fatalf("event %d: the key = %+v, want event-builds %d failed on a transport fault", i+1, v, i+1)
|
|
}
|
|
}
|
|
heard(4, runningSubject+".3", running) // a sequence heard is not acted on again
|
|
if v := valueOf(t, r.st, r.key); v.EventBuilds != 3 || len(asked) != 0 {
|
|
t.Fatalf("a sequence heard again: the key = %+v, asked %v", v, asked)
|
|
}
|
|
heard(5, runningSubject, running)
|
|
heard(6, runningSubject, running)
|
|
if v := valueOf(t, r.st, r.key); v.EventBuilds != 3 || len(asked) != 1 || asked[0] != r.key {
|
|
t.Fatalf("the fourth event: the key = %+v, asked %v, want nothing built and the operator asked once", v, asked)
|
|
}
|
|
// A person's build starts the count again.
|
|
if res := r.b.BuildAgain(context.Background(), "m", fp1, "Build once more", "operator", 2e9); res.Refused != "" {
|
|
t.Fatalf("built again by hand: %+v", res)
|
|
}
|
|
r.b.Wait()
|
|
if v := valueOf(t, r.st, r.key); v.EventBuilds != 0 {
|
|
t.Fatalf("after a person's build: the key = %+v, want event-builds 0", v)
|
|
}
|
|
}
|
|
|
|
// A manifest-reads-changed with why returned builds again the keys whose step ran on its node, and no other's.
|
|
func TestRetakeOnANodeReturned(t *testing.T) {
|
|
r := newRig(t, goBundle)
|
|
r.consider(t, "modules/m")
|
|
seed(t, r.st, builds.BuilderBuild{State: builds.BuildFailed, By: "novox.builder", Manifest: mdigest('a'),
|
|
Error: "transport fault: connection, node-build-go ace, dial tcp: connection refused"})
|
|
rt := &Retaker{Builder: r.b, Acted: &actedStore{}}
|
|
for seq, c := range []struct {
|
|
node, why string
|
|
built bool
|
|
}{{"g14", "returned", false}, {"ace", "started", false}, {"ace", "returned", true}} {
|
|
data, _ := json.Marshal(nodeengine.ManifestReadsChanged{Node: c.node, Why: c.why})
|
|
if _, err := rt.Heard(context.Background(), "mesh.seat.node-engine.event.manifest-reads-changed."+c.node, data, uint64(seq+1)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r.b.Wait()
|
|
if v := valueOf(t, r.st, r.key); (v.State == builds.BuildDone) != c.built {
|
|
t.Fatalf("%s %s: the key = %+v, want built %v", c.node, c.why, v, c.built)
|
|
}
|
|
}
|
|
}
|