package approval import ( "context" "encoding/json" "fmt" "sort" "strings" "sync" "time" condseat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions" stdio "git.novox.be/novox/mesh-sdk/go" "git.novox.be/novox/mesh-sdk/go/keys" ) // The runner's findings of the gate (design 46 7-approval-mark §4, §16, §19), written through its held writer. const ( KindMarkUnserved = "mark-unserved" KindCallUnanswered = "call-unanswered" KindApprovalsUnreadable = "approvals-unreadable" ) // Kinds are the finding kinds the gate writes through the runner's writer. var Kinds = []string{KindMarkUnserved, KindCallUnanswered, KindApprovalsUnreadable} // FindingWriter is the runner's held finding writer (views.HeldWriter over the contract's Finding). type FindingWriter interface { Write(ctx context.Context, finding condseat.Finding) error Clear(ctx context.Context, scope, id, kind, why string) error } // Deps is what the gate reaches the mesh through: the runner's connection in its one place, fakes in a test. type Deps struct { // Node is the machine the runner serves; Runner its own module's name, the subject of its findings. Node, Runner string // Catalog is the catalog half the marks are read from. Catalog Catalog // Wait waits, within the bound, for the catalog half's first view, and is nil once it is read or why not. Wait func(within time.Duration) error // Findings is the runner's held writer; nil says a finding in the log only. Findings FindingWriter Logf func(string, ...any) Now func() time.Time // After is the clock's pause; nil is time.After. After func(time.Duration) <-chan time.Time // State is the runner's state approvals; nil when its membership binds none, so every marked call is refused (§13). State Store // Publish says an accept on a seat under the module's name, on the runner's own account (§7). Publish func(module, subject string, body any, id string) error // Record reads the router's record of an ask, `.` in the seat's state asks, on the runner's own account (§14). Record func(module, id string) (*stdio.StateEntry, error) // Warranted asks `mesh-hand-acts.warranted` {asker, ask}, each try a kept call open while open says so (§16, A-157). Warranted func(asker, ask string, open func() (bool, error)) error // Bounds is the runner's key-ttl state bound (A-157, A-158); nil keeps no bound. Bounds Bounds // Holder says whether a module holds a seat, from the assigner's view of the seats' holders the runner follows (§19). Holder func(seat string) (bool, error) // Build is the tool runner's own fingerprint, read from its MESH_FINGERPRINT at its start: the build a marked wait's // act binds; empty while unknown, when every marked wait is refused (design 34 every-event §35). Build string // Hear waits within the bound for the first event on the subjects whose body match takes, or the caller's going: a // marked wait's ask's `decided` (every-event §36); false when none came. Hear func(ctx context.Context, subjects []string, within time.Duration, match func(json.RawMessage) bool) (bool, error) // Go runs what goes on beside the call (a run's recording); nil is a goroutine. Go func(func()) } // Gate is the runner's approval gate: every call of a module it serves goes through it before the bundle (§20). type Gate struct { d Deps mu sync.Mutex // registered is each module's tools as its bundle registered them at its last load. registered map[string]map[string]bool // unserved is each module whose marks name a tool its bundle does not register, with those tools. unserved map[string][]string // unmarkedAt is each caller's address it last called unmarked, so a mark added since is named (§12). unmarkedAt map[string]bool // inFlight is each ask whose recording runs now. inFlight map[string]bool // failingSince is when the state approvals began to fail; zero while it is read. failingSince time.Time // served is each module this runner serves, with the build its bundle serves, as its last load said (§18). served map[string]string } // New is a gate over d. func New(d Deps) *Gate { if d.Logf == nil { d.Logf = func(string, ...any) {} } if d.Now == nil { d.Now = time.Now } if d.After == nil { d.After = time.After } if d.Go == nil { d.Go = func(f func()) { go f() } } return &Gate{d: d, registered: map[string]map[string]bool{}, unserved: map[string][]string{}} } // Unserved is, of the module's marked tools, those its bundle does not register (§4): such a bundle is not loaded, its // finding `mark-unserved` raised; a bundle that serves them, or marks that no longer name them, clear it. Before the marks // are read nothing is known, and every marked call is refused until they are (§19). func (g *Gate) Unserved(module, fingerprint string, registered []string) []string { set := map[string]bool{} for _, t := range registered { set[t] = true } g.mu.Lock() g.registered[module] = set if g.served == nil { g.served = map[string]string{} } g.served[module] = fingerprint g.mu.Unlock() if g.d.Catalog == nil || g.d.Catalog.Ready() != nil { return nil } return g.checkServed(Derive(g.d.Catalog, module, fingerprint)) } // checkServed raises or clears the module's `mark-unserved` as its marks and its bundle's registrations stand now, and is // the tools it marks and does not register. func (g *Gate) checkServed(m Marks) []string { g.mu.Lock() set := g.registered[m.Module] var missing []string for t := range m.Tools { if !set[t] { missing = append(missing, t) } } sort.Strings(missing) was := g.unserved[m.Module] if len(missing) > 0 { g.unserved[m.Module] = missing } else { delete(g.unserved, m.Module) } g.mu.Unlock() switch { case len(missing) > 0 && strings.Join(was, ",") != strings.Join(missing, ","): g.finding(m.Module, KindMarkUnserved, true, fmt.Sprintf("%s's marks (%s, catalog revision %s) name %s, which its bundle on %s does not "+ "register: no tool of %s is served here", m.Module, sourceOf(m, missing[0]), m.Revision, strings.Join(missing, ", "), g.d.Node, m.Module), fmt.Sprintf("%s is not served on %s", m.Module, g.d.Node)) case len(missing) == 0 && len(was) > 0: g.finding(m.Module, KindMarkUnserved, false, "a bundle that serves every marked tool, or marks without the tool, is loaded", "") } return missing } func sourceOf(m Marks, tool string) string { return m.Tools[tool] } // finding writes or clears one of the gate's findings: scope machine, its id `.` escaped as a key's name is, // about the runner's module on this machine (lead answer A-154). func (g *Gate) finding(what, kind string, raised bool, summary, headline string) { id := keys.EscapeName(g.d.Node + "." + what) if g.d.Findings == nil { g.d.Logf("[mesh-tools] %s %s: %s", kind, id, summary) return } var err error if raised { err = g.d.Findings.Write(context.Background(), condseat.Finding{Kind: kind, Scope: scopeMachine, ID: id, Severity: condseat.FindingSeverityWarning, Source: "approval", Observer: g.d.Runner, Subject: condseat.Subject{Scope: "module", ID: g.d.Runner, Machine: g.d.Node}, Summary: summary, Headline: headline, At: g.d.Now().UTC()}) } else { err = g.d.Findings.Clear(context.Background(), scopeMachine, id, kind, summary) } if err != nil { g.d.Logf("[mesh-tools] the finding %s of %s is not kept: %v", kind, id, err) } } const scopeMachine = "machine"