Wake a parked build puller on its capacity's own change, so a node started at capacity 0 takes asks once raised (issue 1887)
g14's Go builder sat idle at capacity 2 while delivery builds waited: a puller with no room waited on its wake alone, which only a pause, a resume or a step's end gives, and it read its capacity from the settings file only when woken. The puller now follows the module's config value capacity through the runtime's watch of what mesh-config resolved (stdio.WatchConfig), uses the value it hands, and wakes on each change: no timer, no file watch, and no wait for the node-engine to fill the file again. It says when it parks and when it wakes, with its capacity.
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
package buildstep
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
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
|
||||
// hands each change after with set.
|
||||
type configWatch struct {
|
||||
mu sync.Mutex
|
||||
value string
|
||||
handle func(stdio.ProvisionChange) error
|
||||
asked string
|
||||
}
|
||||
|
||||
func (w *configWatch) watch(key string, handle func(stdio.ProvisionChange) error) error {
|
||||
w.mu.Lock()
|
||||
w.asked, w.handle = key, handle
|
||||
value := w.value
|
||||
w.mu.Unlock()
|
||||
return handle(stdio.ProvisionChange{Kind: "config", Key: key, Value: json.RawMessage(`"` + value + `"`)})
|
||||
}
|
||||
|
||||
func (w *configWatch) set(t *testing.T, value string) {
|
||||
t.Helper()
|
||||
w.mu.Lock()
|
||||
handle, key := w.handle, w.asked
|
||||
w.mu.Unlock()
|
||||
if handle == nil {
|
||||
t.Fatal("the puller watches no config value")
|
||||
}
|
||||
if err := handle(stdio.ProvisionChange{Kind: "config", Key: key, Value: json.RawMessage(`"` + value + `"`)}); err != nil {
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user