Files
mesh-delivery/modules/mesh-delivery-sdk/steprun/pull_seat_test.go
T
jschoubben d29e1426d0 Asker rename R2: every asker of a build seat asks under its seat's own name, and its step is keyed so
A-163: the asker in `accept.<queue>.<asker>` is the asking seat's name (mesh-pipeline-runner,
mesh-catalog, mesh-wants, mesh-builds), never a nickname: the builder asks as mesh-builds (was
"builder"), the indexer's validation as mesh-catalog (was "indexer"), the assigner's runs as
mesh-wants (was "assigner"), the runner's steps on `accept.check.mesh-pipeline-runner` and
`accept.delivery.mesh-pipeline-runner` (were `accept.check.step` and `accept.delivery.step`, where
the holder read `step` as the asker). An asker naming none (the checker) asks on main's subject
(A-184). A build seat's queues are delivery, check and recheck alone (A-170, A-184): buildstep's
AskKinds holds no step, a holder takes a step from check or delivery, and the grants part makes a
worker per queue, never per kind. A holder keys a named ask's step `<asker>.<node>.<id>` and main's
`<node>.<id>` (§481, §483); readers watch their own name's keys. The composition step's step-progress
names the one caller mesh-wants' definition lists for step, read from the seat list (A-294).

Builds: design 47 G §481, §483, §543, §600, A-163, A-170, A-184, A-248, A-294

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-12 18:37:29 +01:00

180 lines
7.1 KiB
Go

package steprun
import (
"context"
"encoding/json"
"fmt"
"strings"
"sync"
"testing"
"time"
nodebuildgo "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/nodebuildgo"
sdkstep "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/buildstep"
stdio "git.novox.be/novox/mesh-sdk/go"
)
// 1887/A11: a bundle's source pulls the kind's worker by its seat's name and settles each ask by the runtime's id.
func TestSeatAsksPullsTheKindsWorkerAndSettlesByID(t *testing.T) {
var pulled []string
var settled []string
src := &SeatAsks{Language: sdkstep.AskGo,
Pull: func(worker string, wait time.Duration) (*stdio.Pulled, error) {
pulled = append(pulled, worker)
if worker != "SEAT_NODE_BUILD_GO_CHECK_worker" {
return nil, nil
}
return &stdio.Pulled{ID: "p9", Worker: worker, Body: []byte(`{"v":1}`), Delivered: 2}, nil
},
Settle: func(id, fate string) error { settled = append(settled, id+" "+fate); return nil }}
if p, err := src.Next(context.Background(), sdkstep.AskDelivery, 0); err != nil || p != nil {
t.Fatalf("delivery = %+v, %v", p, err)
}
p, err := src.Next(context.Background(), sdkstep.AskCheck, time.Second)
if err != nil || p == nil || string(p.Data) != `{"v":1}` || p.Delivered != 2 {
t.Fatalf("check = %+v, %v", p, err)
}
_ = p.InProgress()
_ = p.Ack()
if len(pulled) != 3 || pulled[0] != "SEAT_NODE_BUILD_GO_DELIVERY_worker" || pulled[1] != "SEAT_NODE_BUILD_GO_DELIVERY_named_worker" {
t.Errorf("pulled %v", pulled)
}
if len(settled) != 2 || settled[0] != "p9 progress" || settled[1] != "p9 ack" {
t.Errorf("settled %v", settled)
}
}
// pauseKV is a pauses state in memory: its put, load, delete and keys.
type pauseKV struct {
mu sync.Mutex
m map[string][]byte
}
func (k *pauseKV) put(key string, v any) (uint64, error) {
raw, _ := v.(json.RawMessage)
k.mu.Lock()
defer k.mu.Unlock()
if k.m == nil {
k.m = map[string][]byte{}
}
k.m[key] = raw
return 1, nil
}
func (k *pauseKV) load(key string) ([]byte, error) {
k.mu.Lock()
defer k.mu.Unlock()
return k.m[key], nil
}
func (k *pauseKV) forget(key string) error {
k.mu.Lock()
defer k.mu.Unlock()
delete(k.m, key)
return nil
}
func (k *pauseKV) keys() ([]string, error) {
k.mu.Lock()
defer k.mu.Unlock()
var out []string
for key := range k.m {
out = append(out, key)
}
return out, nil
}
// ADR 0664 §5 (issue 1748): the seat's verb drop. An ask dropped before it is given is refused at pull, naming who dropped it
// and why, its key deleted, and never run; one that runs when it is dropped has its step's context cancelled and ends
// refused the same way, acknowledged; the start deletes drops older than DroppedKeep.
func TestADroppedAskIsRefusedAtPullAndARunningOneIsEnded(t *testing.T) {
for _, running := range []bool{false, true} {
src, st, kv := NewFakeAsks(), newSteps(), &pauseKV{}
a := testAsk(sdkstep.AskDelivery)
started := make(chan struct{})
ran := false
var heard func(stdio.StateChange) error
p := &Puller{Source: src, Language: sdkstep.AskGo, Node: "shanks", Put: st.put, PausePut: kv.put, Load: kv.load, ForgetPause: kv.forget, PauseKeys: kv.keys,
WatchPauses: func(_ string, h func(stdio.StateChange) error) error { heard = h; return nil },
Capacity: func() int { return 1 }, PullWait: 10 * time.Millisecond,
Run: func(ctx context.Context, _ sdkstep.StepAsk) (any, error) {
ran = true
close(started)
<-ctx.Done()
return nil, ctx.Err()
}}
if !running {
if err := p.Drop(a.ID, "mesh-delivery-builder", "superseded"); err != nil {
t.Fatal(err)
}
}
_ = src.PublishAsk(a)
ctx, cancel := context.WithCancel(context.Background())
go p.Pull(ctx)
if running {
<-started
// Another node's holder served the verb: its write reaches this node by the watch of the shared pauses state.
raw, _ := json.Marshal(nodebuildgo.Drop{V: int64(sdkstep.Version), By: "mesh-delivery-builder", Why: "superseded", At: ptr(time.Now())})
kv.put(sdkstep.DropKey(a.ID), json.RawMessage(raw))
if err := heard(stdio.StateChange{Key: sdkstep.DropKey(a.ID), Op: "put", Value: raw}); err != nil {
t.Fatal(err)
}
}
waitFor(t, "the ask ended", func() bool { ack, tm, _ := src.Fates(); return len(ack)+len(tm) == 1 })
cancel()
key, _ := sdkstep.AskStepKey("", "shanks", a.ID)
s, ok := st.get(key)
if !ok || s.State != sdkstep.StepDropped || !strings.Contains(s.Reason, "dropped by mesh-delivery-builder: superseded") || ran != running {
t.Errorf("running %v: step %+v (%v), ran %v", running, s, ok, ran)
}
waitFor(t, fmt.Sprint("the drop key deleted, running ", running), func() bool { kv.mu.Lock(); defer kv.mu.Unlock(); _, held := kv.m[sdkstep.DropKey(a.ID)]; return !held })
}
// The start deletes a drop older than DroppedKeep and keeps a newer one.
kv := &pauseKV{}
old, _ := json.Marshal(nodebuildgo.Drop{V: int64(sdkstep.Version), Why: "old", At: ptr(time.Now().Add(-DroppedKeep - time.Hour))})
fresh, _ := json.Marshal(nodebuildgo.Drop{V: int64(sdkstep.Version), Why: "fresh", At: ptr(time.Now())})
kv.put(sdkstep.DropKey("old"), json.RawMessage(old))
kv.put(sdkstep.DropKey("fresh"), json.RawMessage(fresh))
// Review 1: a module that sets no watch of the drops says so (a dropped ask running here is then not ended).
var said []string
(&Puller{Node: "shanks", Logf: func(f string, a ...any) { said = append(said, fmt.Sprintf(f, a...)) }}).watchDrops()
if len(said) != 1 || !strings.Contains(said[0], "no watch of the drops") {
t.Errorf("a puller with no watch said %q", said)
}
p := &Puller{Load: kv.load, ForgetPause: kv.forget, PauseKeys: kv.keys}
p.sweepDropped()
if keys, _ := kv.keys(); len(keys) != 1 || keys[0] != sdkstep.DropKey("fresh") {
t.Errorf("keys after the sweep = %v, want the fresh drop alone", keys)
}
}
func ptr[T any](v T) *T { return &v }
// Design 47 G §544 (BG3): a holder pulls main's worker, then the queue's named worker; while its membership does not list the
// named worker it takes main's alone and says `named worker not bound` once.
func TestSeatAsksPullsTheNamedWorkerAndSaysWhenItIsNotBound(t *testing.T) {
var said []string
bound := false
src := &SeatAsks{Language: sdkstep.AskGo, Logf: func(f string, a ...any) { said = append(said, fmt.Sprintf(f, a...)) },
Pull: func(worker string, wait time.Duration) (*stdio.Pulled, error) {
if worker != "SEAT_NODE_BUILD_GO_CHECK_named_worker" {
return nil, nil
}
if !bound {
return nil, fmt.Errorf("the module pulls no work from %s", worker)
}
return &stdio.Pulled{ID: "n1", Worker: worker, Subject: "mesh.seat.node-build-go.accept.check.mesh-pipeline-runner", Body: []byte(`{"v":1}`), Delivered: 1}, nil
},
Settle: func(string, string) error { return nil }}
for range 2 {
if p, err := src.Next(context.Background(), sdkstep.AskCheck, 0); err != nil || p != nil {
t.Fatalf("unbound: %+v, %v", p, err)
}
}
if len(said) != 1 || !strings.Contains(said[0], "named worker not bound") {
t.Fatalf("said %q, want `named worker not bound` once", said)
}
bound = true
p, err := src.Next(context.Background(), sdkstep.AskCheck, 0)
if err != nil || p == nil || !strings.HasSuffix(p.Subject, ".mesh-pipeline-runner") {
t.Fatalf("bound: %+v, %v", p, err)
}
}