Fold the puller-wake and Served tests into existing tests to stay within the test budget (issue 1887)
mesh/delivery merged: source.main moved to novox/mesh-sdk@4f4d5ca9c956
mesh/delivery-group group fix/puller-wakes-on-capacity merged: every member is merged
mesh/check pass a8fbabbfc373 on 4f4d5ca9c956 | unit 1102 run, 679 kept, 2 skipped | integration 1103 run, 721 kept, 2 skipped | over bound: unit, inte…
mesh/delivery merged: source.main moved to novox/mesh-sdk@4f4d5ca9c956
mesh/delivery-group group fix/puller-wakes-on-capacity merged: every member is merged
mesh/check pass a8fbabbfc373 on 4f4d5ca9c956 | unit 1102 run, 679 kept, 2 skipped | integration 1103 run, 721 kept, 2 skipped | over bound: unit, inte…
This commit is contained in:
@@ -239,8 +239,8 @@ func (p *Puller) Pull(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if ok, capacity, inHand := p.roomNow(); !ok {
|
if ok, capacity, inHand := p.roomNow(); !ok {
|
||||||
// Parked until something wakes it: a capacity change (the config watch), a resume, or a step's end. Said
|
// Parked until something wakes it: a capacity change (the config watch), a resume, or a step's end. Every
|
||||||
// once each way, so a puller that sits idle says why (issue 1887).
|
// park logs once, so a puller that sits idle says why (issue 1887).
|
||||||
p.logf("the %s puller on %s parks: %d of capacity %d in hand, paused %v", p.Language, p.Node, inHand, capacity, p.Paused())
|
p.logf("the %s puller on %s parks: %d of capacity %d in hand, paused %v", p.Language, p.Node, inHand, capacity, p.Paused())
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
package buildstep
|
package buildstep
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// configWatch is a double of stdio.WatchConfig: it hands the value now at the start, as the runtime does, and a test
|
// configWatch is a double of stdio.WatchConfig: it hands the value now at the start, as the runtime does, and a test
|
||||||
@@ -41,51 +37,3 @@ func (w *configWatch) set(t *testing.T, value string) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1887/AC-wake (g14 idle at capacity 2, 2026-10-25): given a puller started at capacity 0 with an ask waiting, when the
|
|
||||||
// module's config value `capacity` moves to 2 for its node, then it takes the ask, with no pause, resume or step end to
|
|
||||||
// wake it.
|
|
||||||
func TestPullWakesWhenItsCapacityMoves(t *testing.T) {
|
|
||||||
src, st := NewFakeAsks(), newSteps()
|
|
||||||
_ = src.PublishAsk(testAsk(delivery.AskDelivery))
|
|
||||||
w := &configWatch{value: "shanks=3,g14=0"}
|
|
||||||
p := &Puller{Source: src, Language: delivery.AskGo, Node: "g14", Put: st.put, PullWait: 10 * time.Millisecond,
|
|
||||||
Capacity: func() int { return 0 }, // the settings file, not refilled yet
|
|
||||||
WatchConfig: w.watch,
|
|
||||||
Run: func(ctx context.Context, a delivery.StepAsk) (any, error) { return nil, nil }}
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
defer cancel()
|
|
||||||
go p.Pull(ctx)
|
|
||||||
waitFor(t, "the watch of the capacity", func() bool { w.mu.Lock(); defer w.mu.Unlock(); return w.handle != nil })
|
|
||||||
time.Sleep(30 * time.Millisecond)
|
|
||||||
if src.Waiting(delivery.AskDelivery) != 1 {
|
|
||||||
t.Fatal("a node at capacity 0 took an ask")
|
|
||||||
}
|
|
||||||
if w.asked != CapacitySetting {
|
|
||||||
t.Fatalf("the puller watches %q, want %q", w.asked, CapacitySetting)
|
|
||||||
}
|
|
||||||
w.set(t, "shanks=3,g14=2")
|
|
||||||
waitFor(t, "the ask taken once the capacity moved to 2", func() bool { a, _, _ := src.Fates(); return len(a) == 1 })
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1887/AC-wake: a value the watch says is no longer answered pulls nothing; a watch that cannot start leaves the puller
|
|
||||||
// on its Capacity (the settings file).
|
|
||||||
func TestPullCapacityWatchRefusedOrUnstarted(t *testing.T) {
|
|
||||||
src, st := NewFakeAsks(), newSteps()
|
|
||||||
p := &Puller{Source: src, Language: delivery.AskGo, Node: "g14", Put: st.put, Capacity: func() int { return 1 },
|
|
||||||
WatchConfig: func(key string, handle func(stdio.ProvisionChange) error) error {
|
|
||||||
return handle(stdio.ProvisionChange{Kind: "config", Key: key, Refused: "the module is no longer on g14"})
|
|
||||||
},
|
|
||||||
Run: func(ctx context.Context, a delivery.StepAsk) (any, error) { return nil, nil }}
|
|
||||||
p.watchCapacity()
|
|
||||||
if p.room() {
|
|
||||||
t.Fatal("a refused capacity value left room")
|
|
||||||
}
|
|
||||||
q := &Puller{Source: src, Language: delivery.AskGo, Node: "g14", Put: st.put, Capacity: func() int { return 1 },
|
|
||||||
WatchConfig: func(string, func(stdio.ProvisionChange) error) error { return errors.New("no runtime") },
|
|
||||||
Run: func(ctx context.Context, a delivery.StepAsk) (any, error) { return nil, nil }}
|
|
||||||
q.watchCapacity()
|
|
||||||
if !q.room() {
|
|
||||||
t.Fatal("a watch that could not start ignored the settings file's capacity of 1")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+70
-19
@@ -3,11 +3,13 @@ package buildstep
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -175,27 +177,76 @@ func (k *kv) load(key string) ([]byte, error) {
|
|||||||
return k.vals[key], nil
|
return k.vals[key], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1887/A2b, 1887/AC-zero: at capacity 0 the puller takes nothing; a capacity raised takes effect at the next pull.
|
// 1887/A2b, 1887/AC-zero, 1887/AC-wake: at capacity 0 the puller takes nothing; a capacity raised takes effect at the
|
||||||
|
// next pull, or, with the config watch (g14 idle at capacity 2, 2026-10-25), at once when the value moves, with no
|
||||||
|
// pause, resume or step end to wake it; a refused value pulls nothing, and a watch that cannot start leaves the
|
||||||
|
// settings file's capacity.
|
||||||
func TestPullAtCapacityZeroTakesNothing(t *testing.T) {
|
func TestPullAtCapacityZeroTakesNothing(t *testing.T) {
|
||||||
src, st := NewFakeAsks(), &kv{}
|
t.Run("settings file", func(t *testing.T) {
|
||||||
_ = src.PublishAsk(testAsk(delivery.AskDelivery))
|
src, st := NewFakeAsks(), &kv{}
|
||||||
var mu sync.Mutex
|
_ = src.PublishAsk(testAsk(delivery.AskDelivery))
|
||||||
capacity := 0
|
var mu sync.Mutex
|
||||||
p := &Puller{Source: src, Language: delivery.AskGo, Node: "novox", Put: st.put, PullWait: 10 * time.Millisecond,
|
capacity := 0
|
||||||
Capacity: func() int { mu.Lock(); defer mu.Unlock(); return capacity },
|
p := &Puller{Source: src, Language: delivery.AskGo, Node: "novox", Put: st.put, PullWait: 10 * time.Millisecond,
|
||||||
Run: func(ctx context.Context, a delivery.StepAsk) (any, error) { return nil, nil }}
|
Capacity: func() int { mu.Lock(); defer mu.Unlock(); return capacity },
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
Run: func(ctx context.Context, a delivery.StepAsk) (any, error) { return nil, nil }}
|
||||||
defer cancel()
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
go p.Pull(ctx)
|
defer cancel()
|
||||||
time.Sleep(50 * time.Millisecond)
|
go p.Pull(ctx)
|
||||||
if src.Waiting(delivery.AskDelivery) != 1 {
|
time.Sleep(50 * time.Millisecond)
|
||||||
t.Fatal("a node at capacity 0 took an ask")
|
if src.Waiting(delivery.AskDelivery) != 1 {
|
||||||
|
t.Fatal("a node at capacity 0 took an ask")
|
||||||
|
}
|
||||||
|
mu.Lock()
|
||||||
|
capacity = 1
|
||||||
|
mu.Unlock()
|
||||||
|
p.Wake()
|
||||||
|
waitFor(t, "the ask taken at capacity 1", func() bool { a, _, _ := src.Fates(); return len(a) == 1 })
|
||||||
|
})
|
||||||
|
t.Run("config watch wakes", func(t *testing.T) {
|
||||||
|
src, st := NewFakeAsks(), newSteps()
|
||||||
|
_ = src.PublishAsk(testAsk(delivery.AskDelivery))
|
||||||
|
w := &configWatch{value: "shanks=3,g14=0"}
|
||||||
|
p := &Puller{Source: src, Language: delivery.AskGo, Node: "g14", Put: st.put, PullWait: 10 * time.Millisecond,
|
||||||
|
Capacity: func() int { return 0 }, // the settings file, not refilled yet
|
||||||
|
WatchConfig: w.watch,
|
||||||
|
Run: func(ctx context.Context, a delivery.StepAsk) (any, error) { return nil, nil }}
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
go p.Pull(ctx)
|
||||||
|
waitFor(t, "the watch of the capacity", func() bool { w.mu.Lock(); defer w.mu.Unlock(); return w.handle != nil })
|
||||||
|
time.Sleep(30 * time.Millisecond)
|
||||||
|
if src.Waiting(delivery.AskDelivery) != 1 {
|
||||||
|
t.Fatal("a node at capacity 0 took an ask")
|
||||||
|
}
|
||||||
|
if w.asked != CapacitySetting {
|
||||||
|
t.Fatalf("the puller watches %q, want %q", w.asked, CapacitySetting)
|
||||||
|
}
|
||||||
|
w.set(t, "shanks=3,g14=2")
|
||||||
|
waitFor(t, "the ask taken once the capacity moved to 2", func() bool { a, _, _ := src.Fates(); return len(a) == 1 })
|
||||||
|
})
|
||||||
|
for _, c := range []struct {
|
||||||
|
name string
|
||||||
|
watch func(string, func(stdio.ProvisionChange) error) error
|
||||||
|
room bool
|
||||||
|
}{
|
||||||
|
{"refused value leaves no room", func(key string, handle func(stdio.ProvisionChange) error) error {
|
||||||
|
return handle(stdio.ProvisionChange{Kind: "config", Key: key, Refused: "the module is no longer on g14"})
|
||||||
|
}, false},
|
||||||
|
{"watch that cannot start keeps the settings file", func(string, func(stdio.ProvisionChange) error) error {
|
||||||
|
return errors.New("no runtime")
|
||||||
|
}, true},
|
||||||
|
} {
|
||||||
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
p := &Puller{Source: NewFakeAsks(), Language: delivery.AskGo, Node: "g14", Put: newSteps().put,
|
||||||
|
Capacity: func() int { return 1 }, WatchConfig: c.watch,
|
||||||
|
Run: func(ctx context.Context, a delivery.StepAsk) (any, error) { return nil, nil }}
|
||||||
|
p.watchCapacity()
|
||||||
|
if p.room() != c.room {
|
||||||
|
t.Fatalf("room = %v, want %v", p.room(), c.room)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
mu.Lock()
|
|
||||||
capacity = 1
|
|
||||||
mu.Unlock()
|
|
||||||
p.Wake()
|
|
||||||
waitFor(t, "the ask taken at capacity 1", func() bool { a, _, _ := src.Fates(); return len(a) == 1 })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1887/A2b: a paused puller takes nothing, its pause is kept on the pause key, and a puller started again on that state
|
// 1887/A2b: a paused puller takes nothing, its pause is kept on the pause key, and a puller started again on that state
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
package stdio
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 1887/AC-wake: code started before Serve (a build puller's watch of its capacity) waits on Served, which closes once
|
|
||||||
// Serve has the runtime's channel, so it asks the runtime then and not before.
|
|
||||||
func TestServedClosesOnceServing(t *testing.T) {
|
|
||||||
served, servedOnce = make(chan struct{}), sync.Once{}
|
|
||||||
select {
|
|
||||||
case <-Served():
|
|
||||||
t.Fatal("Served closed before Serve")
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
inR, inW := io.Pipe()
|
|
||||||
defer inW.Close()
|
|
||||||
go func() { _ = ServeOn("", nil, inR, io.Discard) }()
|
|
||||||
select {
|
|
||||||
case <-Served():
|
|
||||||
case <-time.After(2 * time.Second):
|
|
||||||
t.Fatal("Served did not close once serving")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,6 +16,22 @@ func TestABundleEmitsThroughTheRuntimeAndServesAsItsModule(t *testing.T) {
|
|||||||
t.Setenv(ServedAs, "chime")
|
t.Setenv(ServedAs, "chime")
|
||||||
t.Setenv("MESH_MODULE", "chime")
|
t.Setenv("MESH_MODULE", "chime")
|
||||||
t.Setenv("MESH_NODE", "anchor")
|
t.Setenv("MESH_NODE", "anchor")
|
||||||
|
// 1887/AC-wake: Served closes once Serve has the runtime's channel, not before; the globals are put back after.
|
||||||
|
oldServed := served
|
||||||
|
served, servedOnce = make(chan struct{}), sync.Once{}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
served, servedOnce = oldServed, sync.Once{}
|
||||||
|
select {
|
||||||
|
case <-oldServed: // it was closed: its Once is spent too
|
||||||
|
servedOnce.Do(func() {})
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
})
|
||||||
|
select {
|
||||||
|
case <-Served():
|
||||||
|
t.Fatal("Served closed before Serve")
|
||||||
|
default:
|
||||||
|
}
|
||||||
inR, inW := io.Pipe()
|
inR, inW := io.Pipe()
|
||||||
outR, outW := io.Pipe()
|
outR, outW := io.Pipe()
|
||||||
go func() {
|
go func() {
|
||||||
@@ -50,6 +66,11 @@ func TestABundleEmitsThroughTheRuntimeAndServesAsItsModule(t *testing.T) {
|
|||||||
if got := next()["result"].(map[string]any)["serverInfo"].(map[string]any)["name"]; got != "chime" {
|
if got := next()["result"].(map[string]any)["serverInfo"].(map[string]any)["name"]; got != "chime" {
|
||||||
t.Fatalf("served as %v, want the module the runtime named", got)
|
t.Fatalf("served as %v, want the module the runtime named", got)
|
||||||
}
|
}
|
||||||
|
select {
|
||||||
|
case <-Served():
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatal("Served did not close once serving")
|
||||||
|
}
|
||||||
send(map[string]any{"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": map[string]any{"name": "ring", "arguments": map[string]any{}}})
|
send(map[string]any{"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": map[string]any{"name": "ring", "arguments": map[string]any{}}})
|
||||||
asked := next()
|
asked := next()
|
||||||
if asked["method"] != "mesh/publish" {
|
if asked["method"] != "mesh/publish" {
|
||||||
|
|||||||
Reference in New Issue
Block a user