840 lines
29 KiB
Go
840 lines
29 KiB
Go
package selfcompose
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/compose/frombus"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
|
|
"github.com/novox/mesh-host/internal/agent"
|
|
)
|
|
|
|
// Shadow is the node's answer to placement candidates (novox/hq ADR 0559 §4, issue 1126): for every proposal with a key in
|
|
// the placement seat's state `candidates`, it composes this node in shadow from the trunk's placement with the proposal's
|
|
// keys laid over it and its own inputs (the sealed values and boxes it holds among them), compares the result with what
|
|
// the node runs, and hands the answers to be written into the node's own have key, `<node>.node`, under `candidates`.
|
|
// It applies nothing and hands no composition: what runs is still composed from `placement` alone, by the Composer.
|
|
//
|
|
// **Event-driven, like the Composer.** It watches every candidate key, the trunk's placement, every node's record, the
|
|
// indexer's modules, this node's config values, and this node's module haves (what it runs moved); a burst of changes
|
|
// answers once. It runs whether or not the node is placed yet: a node that applies the controller's compositions answers
|
|
// against what it runs from them. An answer is handed only when it changes, and a proposal's answer is dropped when its
|
|
// keys go.
|
|
type Shadow struct {
|
|
c *Composer
|
|
answer func(ctx context.Context, answers map[string]delivery.CandidateAnswer)
|
|
|
|
mu sync.Mutex
|
|
candidates map[string]map[string]candidateKey // by proposal, by node
|
|
answers map[string]delivery.CandidateAnswer
|
|
// wants are the node's wants by module (agent.NodeModule for its own): what a proposal deleting its placement returns
|
|
// it to, the controller's compositions at them.
|
|
wants map[string]delivery.Want
|
|
// ran is, by module, the digest the module's have says runs: what runs, where the agent's list holds only the copy
|
|
// from the declaration kept before the switch (issue 1126, probe #532 on shanks).
|
|
ran map[string]string
|
|
// heldNow is, by module, how its have says the node keeps it today as it runs: "held", "failed", or "runs a build of
|
|
// the old form" (a `build` and no `fp`, which no registered build of the trunk names); "" when it is composed as any
|
|
// other. A candidate that cannot compose it, or holds it only behind one of these, keeps it as today
|
|
// (frombus.Scope.HeldToday, issue 1126).
|
|
heldNow map[string]string
|
|
// answered is, by proposal, the candidate revisions and the input changes (moved) each answer was composed at: a proposal
|
|
// is answered again only when either moved (issue 1126, 2026-10-28: shanks answered probe/1126-seed-7 at revision 84
|
|
// every 13-17 s, woken by its own modules' haves whose health words changed at every look).
|
|
answered map[string]answeredAt
|
|
// moved counts, by input (inputWords), the changes of each of the node's own inputs, and retry names the proposals
|
|
// answered again after a read the bus did not answer: what woke each answer is said with it (issue 1126,
|
|
// 2026-10-28, seed-9's probe: a safety check could not tell a loop from inputs moving).
|
|
moved [len(inputWords)]uint64
|
|
retry map[string]bool
|
|
// owed says an answer met a read the bus did not answer: it is no answer to keep, and its proposal is answered again
|
|
// after a bound (laterAgain), whether or not an input moves (issue 1126, 2026-10-28: ace kept "the vault's own: not
|
|
// yet ... no responders" in its answer to seed-7).
|
|
owed bool
|
|
}
|
|
|
|
// answeredAt is what one proposal's answer was composed at: its keys' revisions, by node, and the changes of each input.
|
|
type answeredAt struct {
|
|
revs string
|
|
moved [len(inputWords)]uint64
|
|
}
|
|
|
|
// inputWords name the shadow's inputs, in Run's order, as an answer says which of them woke it.
|
|
var inputWords = [...]string{"the candidate", "placement", "records", "modules", "values", "haves", "wants"}
|
|
|
|
// wokenBy says what moved between the answer a proposal stands at (had, when done) and the one about to be composed (at):
|
|
// its first answer, a retry, its candidate's revisions, or the inputs that changed, by their words.
|
|
func wokenBy(had answeredAt, done, retry bool, at answeredAt) string {
|
|
switch {
|
|
case retry:
|
|
return "a retry after a read the bus did not answer"
|
|
case !done:
|
|
return "its first answer since the shadow began"
|
|
}
|
|
var words []string
|
|
if had.revs != at.revs {
|
|
words = append(words, inputWords[0])
|
|
}
|
|
for i := 1; i < len(inputWords); i++ {
|
|
if had.moved[i] != at.moved[i] {
|
|
words = append(words, inputWords[i])
|
|
}
|
|
}
|
|
if len(words) == 0 {
|
|
return "nothing that moved"
|
|
}
|
|
return strings.Join(words, ", ")
|
|
}
|
|
|
|
// candidateKey is one candidate key as last read: its value and revision.
|
|
type candidateKey struct {
|
|
c delivery.PlacementCandidate
|
|
rev uint64
|
|
}
|
|
|
|
// NewShadow is the shadow composer of one node; answer writes the node's answers into its own have key.
|
|
func NewShadow(env Env, answer func(ctx context.Context, answers map[string]delivery.CandidateAnswer)) *Shadow {
|
|
return &Shadow{c: New(env), answer: answer, candidates: map[string]map[string]candidateKey{},
|
|
answers: map[string]delivery.CandidateAnswer{}, wants: map[string]delivery.Want{}, ran: map[string]string{}}
|
|
}
|
|
|
|
// Keep runs the shadow until ctx ends, beginning again after backoff(try) whenever it stops: an input it cannot watch
|
|
// yet (the candidates' state not made, or the node's grant of it not loaded at a delivery) or a watch that ended is
|
|
// tried again rather than leaving the node answering nothing for the rest of the link (review of L2b, round 1, finding
|
|
// 2). Each distinct reason is said once.
|
|
func (s *Shadow) Keep(ctx context.Context, backoff func(try int) time.Duration) {
|
|
said := ""
|
|
for try := 1; ctx.Err() == nil; try++ {
|
|
began := time.Now()
|
|
err := s.Run(ctx)
|
|
if ctx.Err() != nil {
|
|
return
|
|
}
|
|
if time.Since(began) > time.Minute {
|
|
try = 1 // it ran: a later stop begins the waits again
|
|
}
|
|
why := "it stopped"
|
|
if err != nil {
|
|
why = err.Error()
|
|
}
|
|
if why != said {
|
|
s.c.env.Say("this node answers no placement candidate for now, and tries again: " + why)
|
|
said = why
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-time.After(backoff(try)):
|
|
}
|
|
}
|
|
}
|
|
|
|
// Run answers until ctx ends or an input's watch ends.
|
|
func (s *Shadow) Run(ctx context.Context) error {
|
|
// Each run watches afresh and starts from nothing read: what changed while it was down (a candidate key deleted)
|
|
// is then not missed, and the watches of a run before are not left open (review of L2b, round 2).
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
s.mu.Lock()
|
|
s.candidates = map[string]map[string]candidateKey{}
|
|
s.wants = map[string]delivery.Want{}
|
|
s.ran = map[string]string{}
|
|
s.answered = map[string]answeredAt{}
|
|
s.moved = [len(inputWords)]uint64{}
|
|
s.retry = map[string]bool{}
|
|
s.c = New(s.c.env)
|
|
s.mu.Unlock()
|
|
c := s.c
|
|
node := c.env.Node
|
|
// In inputWords' order.
|
|
ins := []input{
|
|
{delivery.PlacementCandidatesBucket, ">", nil, nil},
|
|
{delivery.PlacementBucket, ">", (*Composer).takePlacement, nil},
|
|
{delivery.HaveBucket, "*." + delivery.HaveNodeWord, nil, nil},
|
|
{delivery.ModulesBucket, ">", (*Composer).takeModule, nil},
|
|
{ResolvedBucket, node + ".>", (*Composer).takeValue, nil},
|
|
// What this node runs moved: an apply ends in its module's have.
|
|
{delivery.HaveBucket, node + ".>", nil, nil},
|
|
// Its wants: a proposal deleting its placement returns it to the controller's compositions at them.
|
|
{delivery.AssignerWantsBucket, "want." + node + ".>", nil, nil},
|
|
}
|
|
type tagged struct {
|
|
in int
|
|
u agent.Update
|
|
ok bool
|
|
}
|
|
merged := make(chan tagged, 64)
|
|
for i, in := range ins {
|
|
ch, err := c.env.Watch(ctx, in.bucket, in.pattern)
|
|
if err != nil {
|
|
return fmt.Errorf("the node answers placement candidates from %s, which cannot be watched: %w", in.bucket, err)
|
|
}
|
|
go func(i int, ch <-chan agent.Update) {
|
|
for u := range ch {
|
|
select {
|
|
case merged <- tagged{in: i, u: u, ok: true}:
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
select {
|
|
case merged <- tagged{in: i}:
|
|
case <-ctx.Done():
|
|
}
|
|
}(i, ch)
|
|
}
|
|
caught := make([]bool, len(ins))
|
|
ready, dirty := false, false
|
|
// Not ready after a minute is said, naming each input not read yet, in the journal and in an answer to each proposal it
|
|
// knows: a shadow that waits is never silent (issue 1126: ace, g14 and shanks answered nothing to seed #531).
|
|
waiting := time.NewTimer(notReadyAfter)
|
|
defer waiting.Stop()
|
|
var again <-chan time.Time
|
|
tries := 0
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil
|
|
case <-again:
|
|
again = nil
|
|
dirty = true
|
|
case <-waiting.C:
|
|
if !ready {
|
|
s.notReady(ctx, ins, caught)
|
|
}
|
|
continue
|
|
case t := <-merged:
|
|
if !t.ok {
|
|
return fmt.Errorf("the watch of %s ended; the node answers no placement candidate until its agent starts again", ins[t.in].bucket)
|
|
}
|
|
// Only a change of what an answer is composed from answers again: a key written again with the value it held,
|
|
// this node's own key (its answers), or a module's have whose health or words moved but not what it runs, is
|
|
// none (issue 1126, 2026-10-28: each look rewrote shanks' unhealthy modules' haves, and each woke a full answer).
|
|
changed := false
|
|
switch {
|
|
case t.u.Caught:
|
|
caught[t.in] = true
|
|
case t.in == 0:
|
|
changed = s.takeCandidate(t.u)
|
|
case t.in == 2:
|
|
changed = s.takeRecord(t.u)
|
|
case t.in == 5:
|
|
folded := c.takeModuleHave(t.u)
|
|
changed = s.takeRan(t.u) || folded
|
|
case t.in == 6:
|
|
changed = s.takeWant(t.u)
|
|
default:
|
|
changed = s.takeChanged(ins[t.in], t.u)
|
|
}
|
|
if changed {
|
|
if t.in != 0 {
|
|
s.moved[t.in]++ // the candidates' own changes are their revisions
|
|
}
|
|
dirty = true
|
|
}
|
|
if len(merged) > 0 {
|
|
continue
|
|
}
|
|
}
|
|
if !ready {
|
|
if ready = all(caught); !ready {
|
|
continue
|
|
}
|
|
dirty = true
|
|
}
|
|
if dirty {
|
|
dirty = false
|
|
s.answerAll(ctx)
|
|
if s.owed {
|
|
wait := LaterWait(tries)
|
|
tries++
|
|
c.env.Say(fmt.Sprintf("this node answers again in %s the placement candidates whose answer met a bus that did not answer", wait))
|
|
again = time.After(wait)
|
|
} else {
|
|
tries, again = 0, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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.
|
|
func revsOf(keys map[string]candidateKey) string {
|
|
nodes := make([]string, 0, len(keys))
|
|
for n := range keys {
|
|
nodes = append(nodes, n)
|
|
}
|
|
sort.Strings(nodes)
|
|
var b strings.Builder
|
|
for _, n := range nodes {
|
|
fmt.Fprintf(&b, "%s=%d;", n, keys[n].rev)
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
// takeCandidate takes one candidate key, and says whether it changed anything.
|
|
func (s *Shadow) takeCandidate(u agent.Update) bool {
|
|
proposal, node, err := delivery.ParsePlacementCandidateKey(u.Key)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if u.Deleted {
|
|
if _, ok := s.candidates[proposal][node]; !ok {
|
|
return false
|
|
}
|
|
delete(s.candidates[proposal], node)
|
|
if len(s.candidates[proposal]) == 0 {
|
|
delete(s.candidates, proposal)
|
|
}
|
|
return true
|
|
}
|
|
c, err := delivery.DecodePlacementCandidate(u.Value)
|
|
if err != nil || c.Proposal != proposal || (c.Placement != nil && c.Placement.Node != node) {
|
|
// Not one: read as gone.
|
|
if _, ok := s.candidates[proposal][node]; ok {
|
|
delete(s.candidates[proposal], node)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
if s.candidates[proposal] == nil {
|
|
s.candidates[proposal] = map[string]candidateKey{}
|
|
}
|
|
s.candidates[proposal][node] = candidateKey{c: c, rev: u.Revision}
|
|
return true
|
|
}
|
|
|
|
// takeRecord takes a node's record, and says whether it changed: the node's own key changes with each answer written,
|
|
// which moves nothing composed.
|
|
func (s *Shadow) takeRecord(u agent.Update) bool {
|
|
node, _, isNode, err := delivery.ParseHaveKey(u.Key)
|
|
if err != nil || !isNode {
|
|
return false
|
|
}
|
|
c := s.c
|
|
c.mu.Lock()
|
|
before, had := c.records[node]
|
|
c.mu.Unlock()
|
|
c.takeRecord(u)
|
|
c.mu.Lock()
|
|
after, has := c.records[node]
|
|
c.mu.Unlock()
|
|
return had != has || (has && !delivery.SameNodeRecord(before, after))
|
|
}
|
|
|
|
// How long the shadow waits before it says it is not ready, and how long a proposal's answer runs before it is said slow.
|
|
var notReadyAfter, slowAfter = time.Minute, time.Minute
|
|
|
|
// notReady says which inputs the shadow has not read yet, and answers each proposal it knows refused for it.
|
|
func (s *Shadow) notReady(ctx context.Context, ins []input, caught []bool) {
|
|
var waits []string
|
|
for i, in := range ins {
|
|
if !caught[i] {
|
|
waits = append(waits, in.bucket+" ("+in.pattern+")")
|
|
}
|
|
}
|
|
why := fmt.Sprintf("the node has not read %s after %s; it answers once it has", strings.Join(waits, ", "), notReadyAfter)
|
|
s.c.env.Say("this node answers no placement candidate yet: " + why)
|
|
s.mu.Lock()
|
|
next := map[string]delivery.CandidateAnswer{}
|
|
for proposal, keys := range s.candidates {
|
|
own, ok := keys[s.c.env.Node]
|
|
if !ok {
|
|
continue // no key of this node's: nothing asks it to answer
|
|
}
|
|
next[proposal] = delivery.CandidateAnswer{Proposal: proposal, Revision: own.rev, Refused: why, Verdict: delivery.CandidateRefused}
|
|
}
|
|
s.answers = next
|
|
s.mu.Unlock()
|
|
if len(next) > 0 && s.answer != nil {
|
|
s.answer(ctx, next)
|
|
}
|
|
}
|
|
|
|
// answerAll answers every proposal, the newest candidate first, and hands the answers each time one changes (issue 1126,
|
|
// T3: ace answered a closed pull request's group for 22 minutes before the newer one, all handed at the end). Newest
|
|
// is the highest revision of the proposal's keys: a withdrawn proposal's candidate is the older and waits; the seat
|
|
// drops a closed one's keys, which ends its answer here (a node does not read the forge). Each answer says when it
|
|
// starts, every progressEvery while it runs what it waits on, and when it ends (T2: the node said nothing for 22
|
|
// minutes).
|
|
func (s *Shadow) answerAll(ctx context.Context) {
|
|
c := s.c
|
|
node := c.env.Node
|
|
s.mu.Lock()
|
|
type listed struct {
|
|
proposal string
|
|
rev uint64
|
|
at answeredAt
|
|
}
|
|
var proposals []listed
|
|
for g, keys := range s.candidates {
|
|
l := listed{proposal: g, at: answeredAt{revs: revsOf(keys), moved: s.moved}}
|
|
for _, k := range keys {
|
|
l.rev = max(l.rev, k.rev)
|
|
}
|
|
proposals = append(proposals, l)
|
|
}
|
|
// A proposal whose keys' revisions and the node's inputs are those its answer stands at is not answered again.
|
|
for g := range s.answered {
|
|
if _, ok := s.candidates[g]; !ok {
|
|
delete(s.answered, g)
|
|
}
|
|
}
|
|
// Each proposal's last answer stands until it is answered again; a proposal whose keys went is dropped.
|
|
next := map[string]delivery.CandidateAnswer{}
|
|
for _, l := range proposals {
|
|
if a, ok := s.answers[l.proposal]; ok {
|
|
next[l.proposal] = a
|
|
}
|
|
}
|
|
s.owed = false
|
|
s.mu.Unlock()
|
|
sort.Slice(proposals, func(i, j int) bool {
|
|
if proposals[i].rev != proposals[j].rev {
|
|
return proposals[i].rev > proposals[j].rev
|
|
}
|
|
return proposals[i].proposal < proposals[j].proposal
|
|
})
|
|
hand := func() {
|
|
s.mu.Lock()
|
|
same := len(next) == len(s.answers)
|
|
for g, a := range next {
|
|
if old, ok := s.answers[g]; !ok || !sameAnswer(old, a) {
|
|
same = false
|
|
}
|
|
}
|
|
out := make(map[string]delivery.CandidateAnswer, len(next))
|
|
for g, a := range next {
|
|
out[g] = a
|
|
}
|
|
if !same {
|
|
s.answers = out
|
|
}
|
|
s.mu.Unlock()
|
|
if !same && s.answer != nil {
|
|
given := make(map[string]delivery.CandidateAnswer, len(out))
|
|
for g, a := range out {
|
|
given[g] = a
|
|
}
|
|
s.answer(ctx, given)
|
|
c.env.Say(fmt.Sprintf("%s answered %d placement candidate proposal(s)", node, len(given)))
|
|
}
|
|
}
|
|
for _, l := range proposals {
|
|
if ctx.Err() != nil {
|
|
return
|
|
}
|
|
proposal := l.proposal
|
|
s.mu.Lock()
|
|
at, done := s.answered[proposal]
|
|
retry := s.retry[proposal]
|
|
s.mu.Unlock()
|
|
if done && at == l.at {
|
|
continue
|
|
}
|
|
began := time.Now()
|
|
c.env.Say(fmt.Sprintf("answering placement candidate %s (revision %d; woken by %s)", proposal, l.rev, wokenBy(at, done, retry, l.at)))
|
|
stop := make(chan struct{})
|
|
go func() {
|
|
tick := time.NewTicker(progressEvery)
|
|
defer tick.Stop()
|
|
for {
|
|
select {
|
|
case <-stop:
|
|
return
|
|
case <-tick.C:
|
|
c.env.Say(fmt.Sprintf("the answer to %s has run %s and waits on %s", proposal,
|
|
time.Since(began).Round(time.Second), c.waitingOn()))
|
|
}
|
|
}
|
|
}()
|
|
missed := c.missed.Load()
|
|
a, ok := s.answerProposal(ctx, proposal)
|
|
close(stop)
|
|
provisional := c.missed.Load() > missed && ctx.Err() == nil
|
|
took := time.Since(began)
|
|
if took > slowAfter {
|
|
c.env.Say(fmt.Sprintf("the answer to %s took %s", proposal, took.Round(time.Second)))
|
|
if ok {
|
|
// Words that do not move with the time it took, so an answer the same as before is not written again.
|
|
a.Note = strings.TrimPrefix(a.Note+"; ", "; ") + "this answer took over " + slowAfter.String() + " to compose"
|
|
}
|
|
}
|
|
if ok {
|
|
a = delivery.FitAnswer(a, delivery.MaxAnswerBytes)
|
|
next[proposal] = a
|
|
c.env.Say(fmt.Sprintf("answered placement candidate %s in %s: %s", proposal, took.Round(time.Millisecond), a.Verdict))
|
|
} else {
|
|
delete(next, proposal)
|
|
c.env.Say(fmt.Sprintf("placement candidate %s does not place this node: nothing to answer", proposal))
|
|
}
|
|
s.mu.Lock()
|
|
if provisional {
|
|
// What the bus did not answer is no answer: handed as it stands, and composed again after a bound.
|
|
s.owed = true
|
|
s.retry[proposal] = true
|
|
delete(s.answered, proposal)
|
|
c.env.Say(fmt.Sprintf("the answer to %s met a read on the bus that did not answer: it is answered again", proposal))
|
|
} else {
|
|
s.answered[proposal] = l.at
|
|
delete(s.retry, proposal)
|
|
}
|
|
s.mu.Unlock()
|
|
hand()
|
|
}
|
|
hand() // a proposal whose keys went, when no proposal is left to answer
|
|
}
|
|
|
|
// How often a running answer says what it waits on.
|
|
var progressEvery = time.Minute
|
|
|
|
// answerProposal is the node's answer to one proposal, and false when the node has nothing to answer: neither the trunk nor
|
|
// the proposal places it.
|
|
func (s *Shadow) answerProposal(ctx context.Context, proposal string) (delivery.CandidateAnswer, bool) {
|
|
c := s.c
|
|
node := c.env.Node
|
|
defer c.waiting("composing")()
|
|
s.mu.Lock()
|
|
keys := s.candidates[proposal]
|
|
given := make(map[string]*delivery.Placement, len(keys))
|
|
for n, k := range keys {
|
|
if k.c.Reached && k.c.Placement == nil {
|
|
continue // reached, with no key of its own: nothing laid over it
|
|
}
|
|
given[n] = k.c.Placement
|
|
}
|
|
// The revision answered is the node's own candidate key's; a node with none is not asked.
|
|
own, mine := keys[node]
|
|
rev := own.rev
|
|
s.mu.Unlock()
|
|
|
|
c.mu.Lock()
|
|
modules := make(map[string]delivery.Module, len(c.modules))
|
|
for k, v := range c.modules {
|
|
modules[k] = v
|
|
}
|
|
c.mu.Unlock()
|
|
if !mine {
|
|
return delivery.CandidateAnswer{}, false
|
|
}
|
|
|
|
var running []delivery.Composed
|
|
if c.env.Running != nil {
|
|
running = c.env.Running()
|
|
}
|
|
if p, ok := given[node]; ok && p == nil && !own.c.Reached {
|
|
// The proposal deletes this node's placement key: the node then returns to the controller's compositions, as one the
|
|
// catalogue does not place (endWhenUnplaced). Those at its wants are compared with what runs (issue 1126, the
|
|
// seed's revert, whose check read "would stop running node").
|
|
s.mu.Lock()
|
|
wants := make(map[string]delivery.Want, len(s.wants))
|
|
for m, w := range s.wants {
|
|
wants[m] = w
|
|
}
|
|
s.mu.Unlock()
|
|
return returning(ctx, c.get, node, wants, running, proposal, rev), true
|
|
}
|
|
|
|
placement := given[node]
|
|
if placement == nil {
|
|
c.mu.Lock()
|
|
p := c.placements[node]
|
|
c.mu.Unlock()
|
|
placement = &p
|
|
}
|
|
// What it runs inside its own composition, of what the candidate places (the seed's trunk places nothing).
|
|
c.mu.Lock()
|
|
inOwn := c.inOwnOf(*placement)
|
|
c.mu.Unlock()
|
|
fps := shadowBuilds(running, *placement, modules, inOwn)
|
|
kept := map[string]string{}
|
|
for module, fp := range fps {
|
|
if mod, ok := modules[module]; ok && mod.FP2 == fp {
|
|
continue
|
|
}
|
|
kept[module] = fp // composed from that build (frombus.Scope.KeptAt, issue 1126)
|
|
}
|
|
// The boxes it runs, by what each module's have and its own key say after a restart (Held), are reused for the
|
|
// modules it runs: a value sealed again is no difference.
|
|
held := map[string]delivery.Want{agent.NodeModule: {}}
|
|
for module := range fps {
|
|
held[module] = delivery.Want{FP: fps[module]}
|
|
}
|
|
c.mu.Lock()
|
|
c.wants = held
|
|
c.mu.Unlock()
|
|
a := frombus.Answer(ctx, reader{c: c}, c.env.Mesh, frombus.Scope{Node: node, KeptAt: kept, Placements: given, InOwn: inOwn,
|
|
SameValue: c.sameValue, HeldToday: s.heldToday()}, fps, running, proposal, rev)
|
|
s.mu.Lock()
|
|
ran := make(map[string]string, len(s.ran))
|
|
for m, d := range s.ran {
|
|
ran[m] = d
|
|
}
|
|
s.mu.Unlock()
|
|
return asHavesSay(a, ran), true
|
|
}
|
|
|
|
// heldToday is what the node holds today: each module whose have's phase is held, and what its own composer holds, the
|
|
// node's own composition ("node") among them when it composes none of its own.
|
|
func (s *Shadow) heldToday() map[string]string {
|
|
out := map[string]string{}
|
|
s.mu.Lock()
|
|
for m, how := range s.heldNow {
|
|
if how != "" {
|
|
out[m] = how
|
|
}
|
|
}
|
|
s.mu.Unlock()
|
|
for _, m := range s.c.heldNow() {
|
|
if _, said := out[m]; !said {
|
|
out[m] = "held"
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// takeRan keeps the digest one of this node's module haves says runs, and says whether it changed: a have whose health
|
|
// or words alone moved runs the same.
|
|
func (s *Shadow) takeRan(u agent.Update) bool {
|
|
node, module, isNode, err := delivery.ParseHaveKey(u.Key)
|
|
if err != nil || isNode || node != s.c.env.Node {
|
|
return false
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
before, had := s.ran[module]
|
|
h, err := delivery.DecodeHave(u.Value)
|
|
if s.heldNow == nil {
|
|
s.heldNow = map[string]string{}
|
|
}
|
|
wasHeld := s.heldNow[module]
|
|
s.heldNow[module] = ""
|
|
if !u.Deleted && err == nil {
|
|
switch {
|
|
case h.Phase == delivery.PhaseHeld:
|
|
s.heldNow[module] = "held"
|
|
case h.Phase == delivery.PhaseFailed:
|
|
s.heldNow[module] = "failed"
|
|
case h.Phase != delivery.PhaseRemoved && h.FP == "" && h.Build != "":
|
|
// A build of the old form: its have names a `build` and no `fp`, which no registered build of the trunk
|
|
// names, so the controller keeps it as it runs (issue 1126: novox's de-spiegel at build-…); a seed keeps it so.
|
|
s.heldNow[module] = "runs a build of the old form"
|
|
}
|
|
}
|
|
holdMoved := wasHeld != s.heldNow[module] // a hold's start or end changes the answer too
|
|
if u.Deleted || err != nil || h.Composed == "" || h.Phase == delivery.PhaseRemoved {
|
|
delete(s.ran, module)
|
|
return had || holdMoved
|
|
}
|
|
s.ran[module] = h.Composed
|
|
return !had || before != h.Composed || holdMoved
|
|
}
|
|
|
|
// asHavesSay is a with each module that differs only from the copy the agent keeps of the declaration before the switch
|
|
// counted the same where the composed digest is the one its have says runs: what runs is the composition at that
|
|
// digest, never the kept copy (issue 1126, probe #532: shanks' xterm, zsh, zsh-autosuggestions and
|
|
// zsh-syntax-highlighting).
|
|
func asHavesSay(a delivery.CandidateAnswer, ran map[string]string) delivery.CandidateAnswer {
|
|
if a.Refused != "" || len(a.Differs) == 0 {
|
|
return a
|
|
}
|
|
kept := a.Differs[:0:0]
|
|
for _, d := range a.Differs {
|
|
if d.Composed != "" && ran[d.Module] == d.Composed {
|
|
a.Same = append(a.Same, d.Module)
|
|
continue
|
|
}
|
|
kept = append(kept, d)
|
|
}
|
|
a.Differs = kept
|
|
sort.Strings(a.Same)
|
|
a.Verdict = delivery.VerdictOf(a)
|
|
return a
|
|
}
|
|
|
|
// shadowBuilds is the build each module placed is composed at in shadow: the one it runs, and for one the candidate
|
|
// adds, the trunk's. A module it runs inside its own composition (inOwn, from its haves: mesh-wireguard, one at a build
|
|
// of the old form) is given none, so it is folded there again, as the node's own composer folds it, rather than
|
|
// composed at the trunk's build as if the candidate added it (issue 1126: nats and photos have a trunk build that no
|
|
// want names).
|
|
func shadowBuilds(running []delivery.Composed, placement delivery.Placement, modules map[string]delivery.Module, inOwn []string) map[string]string {
|
|
fps := map[string]string{}
|
|
for _, r := range running {
|
|
if r.Module != "" && r.FP != "" {
|
|
fps[r.Module] = r.FP
|
|
}
|
|
}
|
|
folded := map[string]bool{}
|
|
for _, m := range inOwn {
|
|
folded[m] = true
|
|
}
|
|
for _, m := range placement.Modules {
|
|
if _, runs := fps[m.Module]; !runs && !folded[m.Module] {
|
|
if mod, ok := modules[m.Module]; ok && mod.FP2 != "" {
|
|
fps[m.Module] = mod.FP2
|
|
}
|
|
}
|
|
}
|
|
return fps
|
|
}
|
|
|
|
func sameAnswer(a, b delivery.CandidateAnswer) bool {
|
|
x, errA := delivery.EncodeHaveNode(delivery.HaveNode{Candidates: map[string]delivery.CandidateAnswer{"g": a}})
|
|
y, errB := delivery.EncodeHaveNode(delivery.HaveNode{Candidates: map[string]delivery.CandidateAnswer{"g": b}})
|
|
return errA == nil && errB == nil && string(x) == string(y)
|
|
}
|
|
|
|
// takeWant takes one of the node's wants, and says whether it changed.
|
|
func (s *Shadow) takeWant(u agent.Update) bool {
|
|
module := ""
|
|
var w delivery.Want
|
|
if _, err := delivery.ParseWantNodeKey(u.Key); err == nil {
|
|
module = agent.NodeModule
|
|
} else if node, m, err := delivery.ParseWantKey(u.Key); err == nil && node == s.c.env.Node {
|
|
module = m
|
|
if !u.Deleted {
|
|
if w, err = delivery.DecodeWant(u.Value); err != nil || w.Removed || w.FP == "" {
|
|
u.Deleted = true
|
|
}
|
|
}
|
|
} else {
|
|
return false
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
old, had := s.wants[module]
|
|
if u.Deleted {
|
|
delete(s.wants, module)
|
|
return had
|
|
}
|
|
s.wants[module] = w
|
|
return !had || old.FP != w.FP
|
|
}
|
|
|
|
// returning is the node's answer to a proposal deleting its placement: it returns to the controller's compositions, each
|
|
// read at the fingerprint its want names in the node's composition bucket and compared with what runs
|
|
// (delivery.CompareComposed). A want the controller holds no composition for, or one that cannot be read, refuses the
|
|
// answer, naming each: returning then would leave that module with nothing to apply.
|
|
func returning(ctx context.Context, get func(ctx context.Context, bucket, key string) ([]byte, error), node string,
|
|
wants map[string]delivery.Want, running []delivery.Composed, proposal string, rev uint64) delivery.CandidateAnswer {
|
|
a := delivery.CandidateAnswer{Proposal: proposal, Revision: rev}
|
|
bucket, err := delivery.ComposedBucketFor(node)
|
|
if err != nil {
|
|
a.Refused, a.Verdict = err.Error(), delivery.CandidateRefused
|
|
return a
|
|
}
|
|
modules := make([]string, 0, len(wants))
|
|
for m := range wants {
|
|
modules = append(modules, m)
|
|
}
|
|
sort.Strings(modules)
|
|
comps := map[string]delivery.Composed{}
|
|
var missing []string
|
|
for _, m := range modules {
|
|
var key string
|
|
if m == agent.NodeModule {
|
|
key, err = delivery.ComposedNodeKey(node)
|
|
} else {
|
|
key, err = delivery.ComposedKey(node, m, wants[m].FP)
|
|
}
|
|
if err != nil {
|
|
missing = append(missing, m+": "+err.Error())
|
|
continue
|
|
}
|
|
raw, err := get(ctx, bucket, key)
|
|
switch {
|
|
case err != nil:
|
|
missing = append(missing, fmt.Sprintf("%s: the controller's composition %s cannot be read: %v", m, key, err))
|
|
continue
|
|
case raw == nil:
|
|
missing = append(missing, fmt.Sprintf("%s: the controller holds no composition %s, which its want names", m, key))
|
|
continue
|
|
}
|
|
comp, err := delivery.DecodeComposed(raw)
|
|
if err != nil {
|
|
missing = append(missing, fmt.Sprintf("%s: the controller's composition %s: %v", m, key, err))
|
|
continue
|
|
}
|
|
comps[key] = comp
|
|
}
|
|
if len(missing) > 0 {
|
|
a.Refused = "returning to the controller's compositions, " + strings.Join(missing, "; ")
|
|
a.Verdict = delivery.CandidateRefused
|
|
return a
|
|
}
|
|
cmp := delivery.CompareComposed(comps, running)
|
|
a.Returns = delivery.ReturnsToController
|
|
a.Same, a.Differs, a.Added, a.Removed = cmp.Same, cmp.Differs, cmp.Added, cmp.Removed
|
|
a.Verdict = delivery.VerdictOf(a)
|
|
return a
|
|
}
|
|
|
|
// sameValue says whether two sealed values open, with this node's key, to the same value; unknown when either does not
|
|
// open (or the node holds no opener). Only the comparison leaves: never a value (issue 1126, the shadow shows its work).
|
|
func (c *Composer) sameValue(a, b string) (same, known bool) {
|
|
if c.env.Unseal == nil {
|
|
return false, false
|
|
}
|
|
x, errA := c.env.Unseal(a)
|
|
y, errB := c.env.Unseal(b)
|
|
if errA != nil || errB != nil {
|
|
return false, false
|
|
}
|
|
return bytes.Equal(x, y), true
|
|
}
|