An ask's asker is the last token of its accept subject; the holder checks the kind against the callers the build seat's definition lists (Puller.Callers, CallersFromDefinitions), refuses an ask of another kind answering error naming the kind and the asker and ending it failed, takes only a build and a run on main's subject, and gives back an ask the seat list does not judge yet. A second worker per queue (AskNamedWorker, filtered to accept.<queue>.*) is pulled after main's; the holder says `named worker not bound` while its membership lists none. Builds: design 47 G §543, §544, §481, A-163, A-170 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
180 lines
7.1 KiB
Go
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)
|
|
}
|
|
}
|