Merge pull request 'The delivery view forgets a write or event the bus refused, so 'on the machines' is not left stale (issue 1126)' (#97) from fix/1126-reach-now into main

This commit was merged in pull request #97.
This commit is contained in:
2026-10-28 15:18:54 +00:00
5 changed files with 140 additions and 41 deletions
@@ -75,12 +75,14 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
}
defer conn.Close()
model.SetWriter(kvWriter{ctx: ctx, js: js})
model.SetMotionEvent(func(e view.MotionEvent) {
if err := emitEvent(ctx, js, cred, e.Type, e.Body, e.ID); err != nil {
logger.Printf("the %s of %s on %s could not be said: %v", e.Type, e.Body.Module, e.Body.Node, err)
model.SetMotionEvent(func(e view.MotionEvent) error {
err := emitEvent(ctx, js, cred, e.Type, e.Body, e.ID)
if err != nil {
logger.Printf("the %s of %s on %s could not be said (said again at its next change): %v", e.Type, e.Body.Module, e.Body.Node, err)
}
return err
})
model.SetDeliveredEvent(func(e view.DeliveredEvent) {
model.SetDeliveredEvent(func(e view.DeliveredEvent) error {
id := fmt.Sprintf("delivered.%d", e.Revision)
if e.Repository != "" { // a repository's reached head (ADR 0556 clause 3)
key, _ := delivery.RepositoryKey(e.Repository)
@@ -90,14 +92,18 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
id += "." + hex.EncodeToString(sum[:6])
}
}
if err := emitEvent(ctx, js, cred, DeliveredEventType, e, id); err != nil {
logger.Printf("the delivery of source revision %d could not be said: %v", e.Revision, err)
err := emitEvent(ctx, js, cred, DeliveredEventType, e, id)
if err != nil {
logger.Printf("the delivery of source revision %d could not be said (said again at the next change the view takes in): %v", e.Revision, err)
}
return err
})
model.SetReachWriter(func(w view.ReachWrite) {
if err := putReach(ctx, js, w); err != nil {
logger.Printf("the reach of %s could not be written (written again at its next change): %v", w.Key, err)
model.SetReachWriter(func(w view.ReachWrite) error {
err := putReach(ctx, js, w)
if err != nil {
logger.Printf("the reach of %s could not be written (written again at the next change the view takes in): %v", w.Key, err)
}
return err
})
model.SetBacklog(view.BacklogOver(js, 5*time.Second))
for _, bucket := range []string{delivery.HaveBucket, view.AssignmentsBucket, view.BuildsBucket, delivery.PresenceBucket, delivery.StuckBucket, delivery.CandidatesBucket, delivery.ModulesBucket, delivery.AsksBucket, delivery.AnswersBucket, delivery.BuilderBuildsBucket, delivery.AssignerWantsBucket, delivery.SourceBucket, delivery.ControllerComposedBucket, delivery.AssignerAsksBucket, delivery.ViewAnswersBucket, delivery.TakenBucket} {
@@ -31,8 +31,9 @@ type ReachWrite struct {
Value []byte
}
// SetReachWriter sets where the view's reach values go; nothing is written until it is set.
func (m *Model) SetReachWriter(put func(ReachWrite)) {
// SetReachWriter sets where the view's reach values go; nothing is written until it is set. A value it answers an error for is
// written again at the next change the view takes in (sendReach).
func (m *Model) SetReachWriter(put func(ReachWrite) error) {
m.mu.Lock()
defer m.mu.Unlock()
m.reachPut = put
@@ -1,6 +1,7 @@
package view
import (
"errors"
"strings"
"testing"
@@ -62,6 +63,7 @@ func TestTheViewDerivesHowFarEachRepositoryReached(t *testing.T) {
holds string // words one of web's holds carries; "" none
waiting string // words web's module web waits with; "" none
notYet []string // the merges the reached head's event says are not yet on the machines
refused bool // the bus refuses every write and event until the watches handed over (a start, issue 1126)
}{
{name: "every node runs the newest build", reached: commitC},
{name: "a node lagging stops the head before the change it lacks", reached: commitB, holds: "web on g14",
@@ -82,16 +84,32 @@ func TestTheViewDerivesHowFarEachRepositoryReached(t *testing.T) {
then: []Entry{taken(web, commitB)}},
{name: "a change whose source revision is not known yet holds no head the source named", reached: commitC,
then: []Entry{mod("web", "src2:"+strings.Repeat("9", 64), commitX, 0, true), mod("web", "src2:"+strings.Repeat("9", 64), commitX, 0, false)}},
// 1126: the view started while the bus did not answer its writes; its reach and its event are made at the next change it
// takes in, though the derivation is the same, so the state reach never keeps an older have than node_haves says.
{name: "a write and an event refused at the start are made at the next change", reached: commitC, refused: true,
then: []Entry{haveFP(t, "ace", "mail", fpMail, delivery.HealthOK)}},
} {
t.Run(tc.name, func(t *testing.T) {
m := New()
var written []ReachWrite
var said []DeliveredEvent
m.SetReachWriter(func(w ReachWrite) { written = append(written, w) })
m.SetDeliveredEvent(func(e DeliveredEvent) {
refuse := tc.refused
errRefused := errors.New("nats: no responders available for request")
m.SetReachWriter(func(w ReachWrite) error {
if refuse {
return errRefused
}
written = append(written, w)
return nil
})
m.SetDeliveredEvent(func(e DeliveredEvent) error {
if refuse {
return errRefused
}
if e.Repository != "" {
said = append(said, e)
}
return nil
})
fill(m, world(t)...)
if len(written) != 0 {
@@ -100,6 +118,7 @@ func TestTheViewDerivesHowFarEachRepositoryReached(t *testing.T) {
for _, b := range reachBuckets {
m.Initial(b)
}
refuse = false
fill(m, tc.then...)
key, _ := delivery.RepositoryKey(web)
r := m.Reach()[key]
@@ -136,6 +155,9 @@ func TestTheViewDerivesHowFarEachRepositoryReached(t *testing.T) {
last = e
}
}
if last.Repository == "" {
t.Errorf("the reached head of %s was never said", web)
}
if strings.Join(last.NotYet, ",") != strings.Join(tc.notYet, ",") {
t.Errorf("not yet = %v, want %v", last.NotYet, tc.notYet)
}
@@ -223,8 +223,8 @@ type Model struct {
asks map[string]askRecord // the gate's asks, by key
wants map[string]wantEntry // the assigner's wants, by <node>.<module>
source *sourceEntry // the gate's source key
deliveredEvent func(DeliveredEvent)
motionEvent func(MotionEvent)
deliveredEvent func(DeliveredEvent) error
motionEvent func(MotionEvent) error
told map[string]bool // the motion events said, by their ids
initial map[string]bool // the buckets whose watch handed over what stood at its start
said uint64 // the source revision whose delivery was said
@@ -249,7 +249,7 @@ type Model struct {
reach map[string]delivery.Reach // the reach derived last, by repository key
reachSaid map[string]uint64 // the reached head's source revision said last, by repository key
reachNotYet map[string]string // the merges said not yet on the machines with it, by repository key
reachPut func(ReachWrite)
reachPut func(ReachWrite) error
}
type presenceEntry struct {
@@ -380,24 +380,78 @@ func (m *Model) Apply(e Entry) {
say, put := m.deliveredEvent, m.reachPut
sayMotion := m.motionEvent
m.mu.Unlock()
if event != nil && say != nil {
say(*event)
if event != nil && say != nil && say(*event) != nil {
m.unsay(*event)
}
m.sendReach(put, say, writes, reached)
m.sayMotion(sayMotion, motion)
}
// sayMotion says the motion events after the model's lock is released; one not said is forgotten as told, so the next change of
// its want, have or stuck key says it again.
func (m *Model) sayMotion(say func(MotionEvent) error, events []MotionEvent) {
if say == nil {
return
}
for _, ev := range events {
if say(ev) != nil {
m.mu.Lock()
delete(m.told, ev.ID)
m.mu.Unlock()
}
}
}
// sendReach writes the reach values and says the reached heads, after the model's lock is released. A value not written or an
// event not said is forgotten as written or said, so the next change of any key the view watches writes or says it again
// (issue 1126: a write refused while the bus was not yet answering left the view's state reach at an older have for good, and
// mesh-delivery.show and knowledge-base.progress said a module not running that node_haves said running).
func (m *Model) sendReach(put func(ReachWrite) error, say func(DeliveredEvent) error, writes []ReachWrite, reached []DeliveredEvent) {
var unwritten []ReachWrite
var unsaid []DeliveredEvent
if put != nil {
for _, w := range writes {
put(w)
if put(w) != nil {
unwritten = append(unwritten, w)
}
}
}
if say != nil {
for _, ev := range reached {
say(ev)
if say(ev) != nil {
unsaid = append(unsaid, ev)
}
}
}
if sayMotion != nil {
for _, ev := range motion {
sayMotion(ev)
if len(unwritten) == 0 && len(unsaid) == 0 {
return
}
m.mu.Lock()
defer m.mu.Unlock()
for _, w := range unwritten {
r, err := delivery.DecodeReach(w.Value)
if prev, ok := m.reach[w.Key]; ok && err == nil && delivery.SameReach(prev, r) {
delete(m.reach, w.Key) // a newer value derived since stands as it is: its own write says whether it landed
}
}
for _, ev := range unsaid {
key, err := delivery.RepositoryKey(ev.Repository)
if err == nil && m.reachSaid[key] == ev.Revision && m.reachNotYet[key] == strings.Join(ev.NotYet, ",") {
m.reachNotYet[key] = unsaidMark
}
}
}
// unsaidMark stands for a reached head's event not said: it equals no list of merges, so the next derivation says it again.
const unsaidMark = "\x00not said"
// unsay forgets that a source revision's delivered event was said, when it could not be said and nothing newer was said since.
func (m *Model) unsay(ev DeliveredEvent) {
m.mu.Lock()
defer m.mu.Unlock()
if m.said == ev.Revision {
m.said = 0
}
}
// DeliveredEvent is what the view says when the source revision is delivered (event `delivered` of the view, issue 1906 D9, ADR
@@ -418,7 +472,7 @@ type DeliveredEvent struct {
}
// SetDeliveredEvent sets where the event of a delivered source revision goes; nothing is said until it is set.
func (m *Model) SetDeliveredEvent(say func(DeliveredEvent)) {
func (m *Model) SetDeliveredEvent(say func(DeliveredEvent) error) {
m.mu.Lock()
defer m.mu.Unlock()
m.deliveredEvent = say
@@ -1191,7 +1245,7 @@ type MotionEvent struct {
}
// SetMotionEvent sets where the events of modules in motion go; nothing is said until it is set.
func (m *Model) SetMotionEvent(say func(MotionEvent)) {
func (m *Model) SetMotionEvent(say func(MotionEvent) error) {
m.mu.Lock()
defer m.mu.Unlock()
m.motionEvent = say
@@ -1265,19 +1319,8 @@ func (m *Model) Initial(bucket string) {
writes, reached := m.reachNow()
put, sayDelivered := m.reachPut, m.deliveredEvent
m.mu.Unlock()
for _, ev := range out {
say(ev)
}
if put != nil {
for _, w := range writes {
put(w)
}
}
if sayDelivered != nil {
for _, ev := range reached {
sayDelivered(ev)
}
}
m.sayMotion(say, out)
m.sendReach(put, sayDelivered, writes, reached)
}
// motionOf is the events to say now about the node and module of a model key, from their want, have and stuck key.
@@ -2,6 +2,7 @@ package view
import (
"encoding/json"
"errors"
"strings"
"testing"
@@ -167,7 +168,15 @@ func TestTheJudgementOfTheMoveIsShownBesideTheWant(t *testing.T) {
func TestTheViewSaysOnceThatASourceRevisionIsDelivered(t *testing.T) {
m := New()
var said []DeliveredEvent
m.SetDeliveredEvent(func(e DeliveredEvent) { said = append(said, e) })
refuse := true // the bus refuses the first try to say it (issue 1126): it is said at the next change, not lost
m.SetDeliveredEvent(func(e DeliveredEvent) error {
if refuse && e.Repository == "" {
refuse = false
return errors.New("nats: no responders available for request")
}
said = append(said, e)
return nil
})
fill(m, sourceE(t, 14355), moduleE(t, "web", fpNew), assign("ace", "web", "b-1", ""),
wantE(t, "ace", "web", delivery.Want{FP: fpNew}))
if len(said) != 0 {
@@ -178,6 +187,10 @@ func TestTheViewSaysOnceThatASourceRevisionIsDelivered(t *testing.T) {
t.Fatalf("said %v for a failed have", said)
}
fill(m, haveFP(t, "ace", "web", fpNew, delivery.HealthOK))
if len(said) != 0 || refuse {
t.Fatalf("said = %+v though the bus refused it", said)
}
fill(m, wantE(t, "ace", "web", delivery.Want{FP: fpNew}))
if len(said) != 1 || said[0].Revision != 14355 || said[0].Group != "g" || said[0].Heads["novox/web"] == "" {
t.Fatalf("said = %+v, want revision 14355 once with its group and heads", said)
}
@@ -234,7 +247,7 @@ func TestADottedModuleNameIsFoundAndKept(t *testing.T) {
// is held or failed or the watchman says it stopped moving; each once for a node, a module and a composition.
func motionLog(m *Model) *[]MotionEvent {
var said []MotionEvent
m.SetMotionEvent(func(e MotionEvent) { said = append(said, e) })
m.SetMotionEvent(func(e MotionEvent) error { said = append(said, e); return nil })
for _, b := range []string{delivery.AssignerWantsBucket, delivery.HaveBucket, delivery.StuckBucket} {
m.Initial(b)
}
@@ -269,6 +282,20 @@ func TestAModuleInMotionIsSaidMovingThenSettledOnceEach(t *testing.T) {
if types(*said) != "moving" {
t.Errorf("said %q after the same state again", types(*said))
}
// 1126: the bus refuses the settled once; it is said at the next change of the have, not lost.
refused := false
m.SetMotionEvent(func(e MotionEvent) error {
if !refused {
refused = true
return errors.New("nats: no responders available for request")
}
*said = append(*said, e)
return nil
})
fill(m, haveFP(t, "ace", "web", fpNew, delivery.HealthOK))
if types(*said) != "moving" || !refused {
t.Errorf("said %q though the bus refused it", types(*said))
}
fill(m, haveFP(t, "ace", "web", fpNew, delivery.HealthOK))
if types(*said) != "moving,settled" || (*said)[1].Body.Outcome != "running" {
t.Errorf("said %q, want settled with its outcome", types(*said))
@@ -330,7 +357,7 @@ func TestANodeThatSettlesWithoutBeingSeenMovingSaysNothing(t *testing.T) {
func TestMotionIsSaidOnlyOnceTheBucketsHaveBeenRead(t *testing.T) {
m := New()
var said []MotionEvent
m.SetMotionEvent(func(e MotionEvent) { said = append(said, e) })
m.SetMotionEvent(func(e MotionEvent) error { said = append(said, e); return nil })
fill(m, wantE(t, "ace", "web", delivery.Want{FP: fpNew}), wantE(t, "ace", "mail", delivery.Want{FP: fpNew}))
m.Initial(delivery.AssignerWantsBucket)
fill(m, haveFP(t, "ace", "web", fpNew, delivery.HealthOK), haveFP(t, "ace", "mail", fpOld, delivery.HealthOK))