package main import ( "context" "fmt" "os" "slices" "sort" "strings" "time" "github.com/novox/mesh-controller/internal/broker" "github.com/novox/mesh-controller/internal/inventory" ) // A machine out of touch when a tier is sent is not sent to and not waited for (novox/hq ADR 0435 §1, issue 1161): the // delivery goes on with the machines heard, says the others were away, and the away ones get the builds by the send // they are given when heard again (ADR 0353, ADR 0360 §5). Away is the silent check's one verdict, machineFacts.silent // over the watchman's presence (ADR 0433 §3), never a second rule; a sleeping machine is away once presence says so. // awayOf is the machines out of touch at a moment: those the silent check calls silent (a sleeping machine // is away once presence says so). A machine // never heard from has not joined and is not away. func awayOf(machines []machineFacts, now time.Time) map[string]bool { out := map[string]bool{} for _, m := range machines { if m.silent(now) { out[m.name] = true } } return out } // judgeAway is the walk's reading of which machines are away as a tier is sent: every node with its last-heard time and // the watchman's verdict on its presence key. A bus that cannot be asked leaves none away, which is what a walk did // before: a machine is not passed on a reading that was not made. var judgeAway = func(ctx context.Context, open *stores, now time.Time) (map[string]bool, error) { nodes, err := open.inventory.Nodes(ctx) if err != nil { return nil, err } address, err := broker.BusAddress() if err != nil { return nil, err } js, err := broker.Dial(address) if err != nil { return nil, err } defer js.Close() machines := make([]machineFacts, len(nodes)) for i, n := range nodes { machines[i] = machineFacts{name: n.Name, lastHeard: n.LastSeen} } if err := withPresence(ctx, js.Conn(), machines, now); err != nil { return nil, err } return awayOf(machines, now), nil } // rememberAway keeps on the plan the machines of running that are away: sorted, each once. func rememberAway(p *inventory.Plan, running []string, away map[string]bool) { for _, n := range running { if away[n] && !slices.Contains(p.Away, n) { p.Away = append(p.Away, n) } } sort.Strings(p.Away) } // runningHeard is the machines of running that the walk waits on for a module: those a send of the walk reached, and // those it did not pass as away. func runningHeard(p inventory.Plan, running []string, s *inventory.PlanModule) []string { return slices.DeleteFunc(slices.Clone(running), func(n string) bool { if !slices.Contains(p.Away, n) { return false } _, sent := s.Rest[n] return !sent }) } // awaySaying is what a plan says of the machines it passed as away, empty when it passed none. func awaySaying(p inventory.Plan) string { var said []string for _, n := range p.Away { said = append(said, n+" was away: sent when it is heard again") } return strings.Join(said, "; ") } // judgedAway reads which machines are away for a tier about to be sent, and says when it cannot. func judgedAway(ctx context.Context, open *stores, now time.Time) map[string]bool { away, err := judgeAway(ctx, open, now) if err != nil { fmt.Fprintf(os.Stderr, "which machines are away cannot be read, so none is passed over: %v\n", err) return nil // empty-on-error: none is passed over on a reading that was not made; the error is said just above } return away }