// Package firstview is the tool runner's picture of the four states it reads of the indexer and the assigner: the assigner's // placement and seat-holders, the indexer's manifests and modules (novox/hq ADR 0615 §1, §3; ADR 0621 §2; issue 1966 TLS-5). // // **It never reads the owners' state.** At start it asks each owner's first-view tool until answered (the owner answers in a // runtime of its own, never through this one: ADR 0621 §1), then follows the owner's views family, one event per changed key; // a gap in the revisions, or an owner whose state was made again, asks the first view once more, said in the log. Past two // minutes without an answer it writes a finding in its own state `findings`, which the condition keeper raises and clears. package firstview import ( "context" "encoding/json" "fmt" conditions "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient" condseat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions" "sort" "sync" "time" "git.novox.be/novox/mesh-sdk/go/views" "github.com/novox/mesh-tools/node-tools/internal/bus" ) // The owners and their first-view tools (the tools' addresses are `.`). const ( Indexer = "mesh-delivery-indexer" Assigner = "mesh-delivery-assigner" ) // Spec is one first view the runner holds. type Spec struct{ Owner, Tool, State string } // Specs are the four. var Specs = []Spec{ {Assigner, "placement", "placement"}, {Assigner, "seat-holders", "seat-holders"}, {Indexer, "manifests", "manifests"}, {Indexer, "modules", "modules"}, } // Name is the name a state is read by, `.`: delivery.PlacementRead, delivery.SeatHoldersRead and the indexer's. func (s Spec) Name() string { return s.Owner + "." + s.State } // Views is the four followers. type Views struct { logf func(string, ...any) followers map[string]*views.Follower // by Spec.Name() byState map[string]*views.Follower // by the state an event names ctx context.Context cancel context.CancelFunc wg sync.WaitGroup deps Deps } // FindingsState is the runner's own state the findings are written in. const FindingsState = "findings" // Deps is what the views reach the bus through: Start makes them from the runner's connection, a test from fakes. type Deps struct { // Module is the runner's own name, the observer of its findings. Module string // Ask calls an owner's tool, `.`. Ask func(address string, body any) (json.RawMessage, error) // Consume hands each event of the owners' views families to deliver, until the stop is called. Consume func(deliver func(bus.Envelope) error) (stop func(), err error) // PutFinding and DeleteFinding keep and remove a finding in the runner's own state. PutFinding func(key string, value json.RawMessage) error DeleteFinding func(key string) error // Bound and Wait are the follower's (zero: two minutes, five seconds). Bound, Wait time.Duration } // Start asks the four first views, each until answered, and follows the owners' views families on the runner's own // consumers. It returns at once: the picture fills as the owners answer. func Start(parent context.Context, conn *bus.Conn, logf func(string, ...any)) (*Views, func()) { return StartWith(parent, DepsOf(conn), logf) } // DepsOf is the views' reach through the runner's own connection: its tool calls, its consumer and its findings state. func DepsOf(conn *bus.Conn) Deps { me := conn.Module() return Deps{Module: me, Ask: func(address string, body any) (json.RawMessage, error) { got, err := conn.Ask(address, body, "") return got.Result, err }, Consume: func(deliver func(bus.Envelope) error) (func(), error) { return conn.ConsumeAs(me, deliver) }, PutFinding: func(key string, value json.RawMessage) error { _, err := conn.StatePut(me, FindingsState, key, value) return err }, DeleteFinding: func(key string) error { return conn.StateDelete(me, FindingsState, key) }, } } // StartWith is Start over deps. func StartWith(parent context.Context, d Deps, logf func(string, ...any)) (*Views, func()) { ctx, cancel := context.WithCancel(parent) v := &Views{logf: logf, ctx: ctx, cancel: cancel, followers: map[string]*views.Follower{}, byState: map[string]*views.Follower{}, deps: d} for _, s := range Specs { s := s call := func(tool string, body any) (json.RawMessage, error) { return d.Ask(s.Owner+"."+tool, body) } f := &views.Follower{Owner: s.Owner, Tool: s.Tool, State: s.State, Ask: views.AskVia(call, s.Tool), Logf: logf, Bound: d.Bound, Wait: d.Wait, Raise: func(st views.Status) { v.finding(st, true) }, Clear: func(st views.Status) { v.finding(st, false) }} v.followers[s.Name()] = f v.byState[s.State] = f v.wg.Add(1) go func() { defer v.wg.Done(); _ = f.Start(ctx) }() } stop, err := d.Consume(v.hear) if err != nil { logf("[mesh-tools] the owners' views are not followed (%v): the pictures are the first views alone until the runner starts again", err) stop = func() {} } return v, func() { stop(); cancel(); v.wg.Wait() } } // hear applies one views event; a gap asks the first view once more. func (v *Views) hear(env bus.Envelope) error { ev, err := views.DecodeEvent(env.Body) if err != nil { return nil // not a views event: nothing to hold, nothing to retry } f := v.byState[ev.State] if f == nil || f.Owner != ev.Owner { return nil } if gap, why := f.Apply(ev); gap { v.logf("[mesh-tools] a gap in %s's %s (%s): its first view %s is asked once more", f.Owner, f.State, why, f.Tool) go func() { _ = f.Again(v.ctx, "a gap: "+why) }() } return nil } // finding writes or removes the runner's finding of a first view unanswered past its bound, in its own state. func (v *Views) finding(st views.Status, raised bool) { key, value, err := conditionOf(views.UnansweredFinding(v.deps.Module, st, time.Now())) if err != nil { v.logf("[mesh-tools] the finding of the first view %s is not made: %v", st.Tool, err) return } if raised { err = v.deps.PutFinding(key, value) } else { err = v.deps.DeleteFinding(key) } if err != nil { v.logf("[mesh-tools] the finding of the first view %s is not kept: %v", st.Tool, err) } } // State is the picture of the state spec names (`.`), every current value by key; an error says the owner has // not answered yet, so nothing is claimed of it. func (v *Views) State(name string) (map[string][]byte, error) { f := v.followers[name] if f == nil { return nil, fmt.Errorf("the tool runner holds no first view of %s", name) } select { case <-f.Ready(): default: return nil, fmt.Errorf("%s's first view %s has not been answered yet", f.Owner, f.Tool) } out := map[string][]byte{} for k, raw := range f.Values() { out[k] = raw } return out, nil } // Entry is one value of the picture, nil when the owner holds none. func (v *Views) Entry(name, key string) ([]byte, error) { all, err := v.State(name) if err != nil { return nil, err } return all[key], nil } // Status is each first view as it stands, by `.`, sorted. func (v *Views) Status() []views.Status { var out []views.Status for _, f := range v.followers { out = append(out, f.Status()) } sort.Slice(out, func(i, j int) bool { return out[i].Owner+out[i].State < out[j].Owner+out[j].State }) return out } // Again asks every first view once more, by hand, for why. func (v *Views) Again(ctx context.Context, why string) []error { var errs []error for _, f := range v.followers { if err := f.Again(ctx, why); err != nil { errs = append(errs, err) } } return errs } // conditionOf is a finding in the layout of the seat mesh-conditions' state findings (its contract, ADR 0634 §1), about a // module, a warning. func conditionOf(f views.Finding) (string, []byte, error) { key := conditions.Key(scopeModule, f.ID, f.Kind) v := condseat.FindingsByKeyValue{V: 1, Key: key, Kind: f.Kind, Observer: f.Observer, Severity: conditions.Warning, Subject: condseat.Subject{Scope: scopeModule, ID: f.ID}, Summary: f.Summary, Headline: f.Headline, Explanation: f.Explanation, Source: f.Observer + "." + f.Kind, At: f.At.UTC().Format(time.RFC3339), Evidence: []condseat.Evidence{{At: f.At.UTC().Format(time.RFC3339), Said: f.Summary}}} if err := conditions.CheckFinding(v); err != nil { return "", nil, err } raw, err := json.Marshal(v) return key, raw, err } // scopeModule is the scope of a finding about a module. const scopeModule = "module"