Merge pull request 'The node composes again only when what it composes from changed (issue 774)' (#187) from fix/compose-only-on-change into main
This commit was merged in pull request #187.
This commit is contained in:
@@ -19,6 +19,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"reflect"
|
||||||
"slices"
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -399,8 +400,7 @@ func (c *Composer) Watch(ctx context.Context, _ string) (<-chan agent.Update, er
|
|||||||
case ins[t.in].changed != nil:
|
case ins[t.in].changed != nil:
|
||||||
dirty = ins[t.in].changed(c, t.u) || dirty
|
dirty = ins[t.in].changed(c, t.u) || dirty
|
||||||
default:
|
default:
|
||||||
ins[t.in].take(c, t.u)
|
dirty = c.takeChanged(ins[t.in], t.u) || dirty
|
||||||
dirty = true
|
|
||||||
}
|
}
|
||||||
if len(merged) > 0 {
|
if len(merged) > 0 {
|
||||||
continue
|
continue
|
||||||
@@ -468,6 +468,65 @@ func (c *Composer) caughtSent() bool {
|
|||||||
return sent
|
return sent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// takeChanged takes u through in.take and says whether the entry it names changed: a want, a placement, a module or a
|
||||||
|
// config value written again with the value it held changes nothing composed, so it makes no pass (issue 774,
|
||||||
|
// 2026-10-31: every put under want.<node>.> made a pass, and each pass asked the vault for the node's own secrets).
|
||||||
|
func (c *Composer) takeChanged(in input, u agent.Update) bool {
|
||||||
|
before, had, known := c.entry(in.bucket, u.Key)
|
||||||
|
in.take(c, u)
|
||||||
|
if !known {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
after, has, _ := c.entry(in.bucket, u.Key)
|
||||||
|
return had != has || !reflect.DeepEqual(before, after)
|
||||||
|
}
|
||||||
|
|
||||||
|
// entry is what the composer holds of one key of bucket, whether it holds it, and whether the bucket is one it knows.
|
||||||
|
func (c *Composer) entry(bucket, key string) (v any, has, known bool) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
switch bucket {
|
||||||
|
case delivery.AssignerWantsBucket:
|
||||||
|
// A want is composed by its fingerprint alone (fingerprints); its words, rule and newest change nothing composed.
|
||||||
|
module := agent.NodeModule
|
||||||
|
if _, err := delivery.ParseWantNodeKey(key); err != nil {
|
||||||
|
node, m, err := delivery.ParseWantKey(key)
|
||||||
|
if err != nil || node != c.env.Node {
|
||||||
|
return nil, false, true
|
||||||
|
}
|
||||||
|
module = m
|
||||||
|
}
|
||||||
|
w, ok := c.wants[module]
|
||||||
|
return w.FP, ok, true
|
||||||
|
case delivery.PlacementBucket:
|
||||||
|
node, err := delivery.ParsePlacementKey(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, true
|
||||||
|
}
|
||||||
|
p, ok := c.placements[node]
|
||||||
|
return p, ok, true
|
||||||
|
case delivery.ModulesBucket:
|
||||||
|
name, err := delivery.ParseModulesKey(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, true
|
||||||
|
}
|
||||||
|
m, ok := c.modules[name]
|
||||||
|
return m, ok, true
|
||||||
|
case ResolvedBucket:
|
||||||
|
rest, ok := strings.CutPrefix(key, c.env.Node+".")
|
||||||
|
if !ok {
|
||||||
|
return nil, false, true
|
||||||
|
}
|
||||||
|
module, k, ok := strings.Cut(rest, ".")
|
||||||
|
if !ok {
|
||||||
|
return nil, false, true
|
||||||
|
}
|
||||||
|
val, ok := c.values[module][k]
|
||||||
|
return val, ok, true
|
||||||
|
}
|
||||||
|
return nil, false, false
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Composer) takeWant(u agent.Update) {
|
func (c *Composer) takeWant(u agent.Update) {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|||||||
@@ -546,6 +546,40 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
|
|||||||
t.Errorf("%s: a pass made %v, want %v", row.name, got, row.pass)
|
t.Errorf("%s: a pass made %v, want %v", row.name, got, row.pass)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Issue 774, 2026-10-31: every put under want.n1.> made a pass, and each pass asked the vault for the node's own
|
||||||
|
// secrets. A key written again with what it held, or a want whose words moved and not its build, makes none.
|
||||||
|
worded, _ := delivery.EncodeWant(delivery.Want{FP: fp, Self: true, Words: "lemurs is held at aaaaaaaaaaaa"})
|
||||||
|
elsewhere, _ := delivery.EncodeWant(delivery.Want{FP: delivery.SourcePrefix2 + strings.Repeat("e", 64), Self: true})
|
||||||
|
for _, row := range []struct {
|
||||||
|
name, bucket, key string
|
||||||
|
value []byte
|
||||||
|
pass bool
|
||||||
|
}{
|
||||||
|
{"lemurs's want again", delivery.AssignerWantsBucket, "want.n1.lemurs", want, false},
|
||||||
|
{"the node's own want again", delivery.AssignerWantsBucket, "want.n1.node", nodeWant, false},
|
||||||
|
{"lemurs's want with other words", delivery.AssignerWantsBucket, "want.n1.lemurs", worded, false},
|
||||||
|
{"the placement again", delivery.PlacementBucket, "placement.n1", place, false},
|
||||||
|
{"lemurs's module again", delivery.ModulesBucket, "lemurs", mod, false},
|
||||||
|
{"lemurs's want at another build", delivery.AssignerWantsBucket, "want.n1.lemurs", elsewhere, true},
|
||||||
|
{"lemurs's want back at its build", delivery.AssignerWantsBucket, "want.n1.lemurs", want, true},
|
||||||
|
} {
|
||||||
|
before := passes()
|
||||||
|
b.put(t, row.bucket, row.key, row.value)
|
||||||
|
if !row.pass {
|
||||||
|
quiet(t, out)
|
||||||
|
} else {
|
||||||
|
for done := false; !done; {
|
||||||
|
select {
|
||||||
|
case <-out:
|
||||||
|
case <-time.After(200 * time.Millisecond):
|
||||||
|
done = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if got := passes() > before; got != row.pass {
|
||||||
|
t.Errorf("%s: a pass made %v, want %v", row.name, got, row.pass)
|
||||||
|
}
|
||||||
|
}
|
||||||
// And today's sequence whole: tool runs at its old build; its want and the trunk move to a build not yet done, and
|
// And today's sequence whole: tool runs at its old build; its want and the trunk move to a build not yet done, and
|
||||||
// the node holds tool, keeping what runs, naming why; the build lands, and with no other change tool is handed at it.
|
// the node holds tool, keeping what runs, naming why; the build lands, and with no other change tool is handed at it.
|
||||||
{
|
{
|
||||||
@@ -629,7 +663,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
|
|||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
n := 0
|
n := 0
|
||||||
for _, s := range b.said {
|
for _, s := range b.said {
|
||||||
if strings.Contains(s, "lemurs is wanted at") {
|
if strings.Contains(s, "lemurs is wanted at "+gone) {
|
||||||
n++
|
n++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -231,7 +230,7 @@ func (s *Shadow) Run(ctx context.Context) error {
|
|||||||
case t.in == 6:
|
case t.in == 6:
|
||||||
changed = s.takeWant(t.u)
|
changed = s.takeWant(t.u)
|
||||||
default:
|
default:
|
||||||
changed = s.takeChanged(ins[t.in], t.u)
|
changed = c.takeChanged(ins[t.in], t.u)
|
||||||
}
|
}
|
||||||
if changed {
|
if changed {
|
||||||
if t.in != 0 {
|
if t.in != 0 {
|
||||||
@@ -264,53 +263,6 @@ func (s *Shadow) Run(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// takeChanged takes u through in.take and says whether the entry it names changed: a placement, a module or a config
|
|
||||||
// value written again with the value it held changes nothing composed.
|
|
||||||
func (s *Shadow) takeChanged(in input, u agent.Update) bool {
|
|
||||||
before, had, known := s.entry(in.bucket, u.Key)
|
|
||||||
in.take(s.c, u)
|
|
||||||
if !known {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
after, has, _ := s.entry(in.bucket, u.Key)
|
|
||||||
return had != has || !reflect.DeepEqual(before, after)
|
|
||||||
}
|
|
||||||
|
|
||||||
// entry is what the composer holds of one key of bucket, whether it holds it, and whether the bucket is one it knows.
|
|
||||||
func (s *Shadow) entry(bucket, key string) (v any, has, known bool) {
|
|
||||||
c := s.c
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
switch bucket {
|
|
||||||
case delivery.PlacementBucket:
|
|
||||||
node, err := delivery.ParsePlacementKey(key)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, true
|
|
||||||
}
|
|
||||||
p, ok := c.placements[node]
|
|
||||||
return p, ok, true
|
|
||||||
case delivery.ModulesBucket:
|
|
||||||
name, err := delivery.ParseModulesKey(key)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, true
|
|
||||||
}
|
|
||||||
m, ok := c.modules[name]
|
|
||||||
return m, ok, true
|
|
||||||
case ResolvedBucket:
|
|
||||||
rest, ok := strings.CutPrefix(key, c.env.Node+".")
|
|
||||||
if !ok {
|
|
||||||
return nil, false, true
|
|
||||||
}
|
|
||||||
module, k, ok := strings.Cut(rest, ".")
|
|
||||||
if !ok {
|
|
||||||
return nil, false, true
|
|
||||||
}
|
|
||||||
val, ok := c.values[module][k]
|
|
||||||
return val, ok, true
|
|
||||||
}
|
|
||||||
return nil, false, false
|
|
||||||
}
|
|
||||||
|
|
||||||
// revsOf is a proposal's keys' revisions, by node, as one word.
|
// revsOf is a proposal's keys' revisions, by node, as one word.
|
||||||
func revsOf(keys map[string]candidateKey) string {
|
func revsOf(keys map[string]candidateKey) string {
|
||||||
nodes := make([]string, 0, len(keys))
|
nodes := make([]string, 0, len(keys))
|
||||||
|
|||||||
Reference in New Issue
Block a user