The builder, its tools bundle, the assigner (process and bundle) and the view's tools bundle answered their seat list's again tool to the modules node-tools and mesh-healers. Their lists are now the operator, seat:node-tool-runner and seat:mesh-healers, resolved by a holder function: - meshcatalogclient's Branches.Claimant (moved from the builder's versions.HolderOf, which now asks it) names the claimant of mesh-module-assigner from the trunk's half, and Branches.SeatHolders follows that claimant's seat-holders view through the part's wiring. It drops the view and follows the new claimant's on a catalog change, asks nothing on a timer, and answers seatlist.HoldersIn. - the assigner answers HoldersIn from its own seat-holders picture (its world in the process, the watched copy in the bundle). The builder's and the view's manifests consume seat:mesh-module-assigner.views.seat-holders. The assigner's seat-holders first-view tool answers both, by module as A-131 has it. Builds: design 47 G A-298, A-295, A-296, A-210, A-131, §717, §616, §614 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
493 lines
30 KiB
Go
493 lines
30 KiB
Go
// Command mesh-delivery-view-tools serves the read tools of the indexer's modules (novox/hq issue 1562): the runtime launches it for a
|
|
// call, it reads the indexer's modules bucket through the view's reads (mesh-delivery-indexer.modules) and writes nothing.
|
|
// The view is the other program: the indexer serves no tool (ADR 0437 §4).
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"mesh-delivery-view/internal/view"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
|
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient/enginereads"
|
|
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient"
|
|
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshwatchdogsclient"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshcatalogclient"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/buildstep"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate"
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
"git.novox.be/novox/mesh-sdk/go/busretry"
|
|
"git.novox.be/novox/mesh-sdk/go/seatdef"
|
|
"git.novox.be/novox/mesh-sdk/go/seatlist"
|
|
"git.novox.be/novox/mesh-sdk/go/views"
|
|
)
|
|
|
|
// ManifestReadsTool lists, per node, what its engine states it reads, by asking the node-engine seat's verb of every node
|
|
// at once (ADR 0618 §3).
|
|
const ManifestReadsTool = "node_manifest_reads"
|
|
|
|
// ManifestReadsBound bounds each node's ask of the survey: the whole call stays within a tool call's 30 s.
|
|
var ManifestReadsBound = 10 * time.Second
|
|
|
|
// askEngine is the survey's caller: the node-engine seat's verb of one machine, through the runtime (stdio.Ask). An error
|
|
// that is no lack of an answer is the engine's own words: it answered and does not serve the verb.
|
|
func askEngine(node, key string, body any) (json.RawMessage, error) {
|
|
raw, err := stdio.Ask(key+"@"+node, body)
|
|
if err != nil {
|
|
words := strings.ToLower(err.Error())
|
|
if !strings.Contains(words, "no responders") && !strings.Contains(words, "timeout") && !strings.Contains(words, "deadline") {
|
|
return nil, &busretry.Answered{Words: err.Error()}
|
|
}
|
|
}
|
|
return raw, err
|
|
}
|
|
|
|
// engineCaller is how node_manifest_reads asks; a test's fake in its place.
|
|
var engineCaller enginereads.Caller = askEngine
|
|
|
|
// manifestReads is node_manifest_reads over r's haves and presence: every node with a record or a presence, asked through
|
|
// call.
|
|
func manifestReads(r view.Reader, call enginereads.Caller) (any, error) {
|
|
set := map[string]bool{}
|
|
records := map[string][]string{}
|
|
if r.HavesState.Keys != nil {
|
|
keys, err := r.HavesState.Keys()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the haves cannot be read: %w", err)
|
|
}
|
|
for _, key := range keys {
|
|
node, _, isNode, err := nodeengineclient.ParseHaveKey(key)
|
|
if err != nil || !isNode {
|
|
continue
|
|
}
|
|
set[node] = true
|
|
if raw, err := r.HavesState.Get(key); err == nil && raw != nil {
|
|
if h, err := nodeengineclient.DecodeHaveNode(raw); err == nil && h.Record != nil {
|
|
records[node] = h.Record.ManifestReads
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if r.PresenceState.Keys != nil {
|
|
if keys, err := r.PresenceState.Keys(); err == nil {
|
|
for _, key := range keys {
|
|
if node, part, _, err := meshwatchdogsclient.ParsePresenceKey(key); err == nil && part == "" && node != "" {
|
|
set[node] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
nodes := make([]string, 0, len(set))
|
|
for node := range set {
|
|
nodes = append(nodes, node)
|
|
}
|
|
rows := enginereads.Survey(call, nodes, func(node string) ([]string, bool) {
|
|
reads, ok := records[node]
|
|
return reads, ok
|
|
}, ManifestReadsBound, time.Now())
|
|
return map[string]any{"nodes": rows}, nil
|
|
}
|
|
|
|
// Tools are the read tools over a modules state, and build_usage over the build modules' states.
|
|
func Tools(r view.Reader, u view.UsageReader) []stdio.Tool {
|
|
return []stdio.Tool{
|
|
{Name: ManifestReadsTool,
|
|
Description: "What each node's engine states it reads of a manifest (manifest words, forms and seat names, novox/hq ADR 0618 " +
|
|
"§3), for the whole mesh in one call: every node with a record or a presence is asked the node-engine seat's verb " +
|
|
"manifest-reads at once, each within " + ManifestReadsBound.String() + ". Per node: stated (with the capabilities and " +
|
|
"when the engine stated them), not served (the engine is built before the verb) or away (nobody answered), and for the " +
|
|
"last two the reads its node record states, which the assigner reads in their place. Read-only.",
|
|
Input: map[string]any{"type": "object", "properties": map[string]any{}},
|
|
Run: func(map[string]any) (any, error) { return manifestReads(r, engineCaller) }},
|
|
{Name: view.StatusTool,
|
|
Description: "The mesh's status, derived by the view from the buckets it reads (novox/hq ADR 0564 §10), nothing written as a " +
|
|
"summary: every open condition of the condition keeper (mesh-conditions), urgent first then oldest; what is quiet (each " +
|
|
"node the watchman says is offline, each key it says stopped moving); each module whose want and have differ on a node " +
|
|
"(moving, or stuck with the have's or the watchman's words), as mesh-sdk's delivery motion derives it; and what " +
|
|
"mesh-healers did in the last seven days, by outcome. A part that cannot be read says so, never as none; well is true " +
|
|
"only when nothing is open, quiet or stuck and every part was read. Read-only.",
|
|
Run: func(map[string]any) (any, error) { return r.Status(time.Now()), nil }},
|
|
{Name: view.ReachReadTool,
|
|
Description: "Whether a merge is on the machines: per repository, the reached head (the newest merge every node runs a build " +
|
|
"containing, as the view derives it from the indexer's modules, the wants and the haves; a merge at or before it is on the " +
|
|
"machines), what holds the next one (a module on a node, the indexer's lag), and per module its fingerprint, the nodes " +
|
|
"that want it, its changes (fingerprint, the merge it first appeared at, its source revision), its floor and the nodes " +
|
|
"where it is not settled or not healthy (novox/hq ADR 0556). Give a repository (owner/name) for one, none for every one; give a module to see only that module of each " +
|
|
"repository and the holds that name it. Answers the delivery reach of a repository and of a module. Read-only.",
|
|
Input: map[string]any{"type": "object", "properties": map[string]any{
|
|
"repository": map[string]any{"type": "string", "description": "the repository, owner/name; none for every repository"},
|
|
"module": map[string]any{"type": "string", "description": "the module name; none for every module"}}},
|
|
Run: func(args map[string]any) (any, error) {
|
|
repository, _ := args["repository"].(string)
|
|
module, _ := args["module"].(string)
|
|
return r.Reach(repository, module)
|
|
}},
|
|
{Name: view.TiersTool,
|
|
Description: "A source revision's tiers, worked out now and never stored (novox/hq ADR 0603 §6): the modules the revision moves " +
|
|
"(each whose fingerprint last changed at it), ordered by the module graph (a module after the bases, the providers " +
|
|
"of the provisions and bus state it awaits and the modules serving the verbs it invokes, among them; novox/hq ADR 0614), " +
|
|
"and for each module what it waits on now and why, named by its edge: its build key, on each node that wants it its want " +
|
|
"(the assigner's rule and words when held back: a verb no running build serves, a reader capability the node-engine " +
|
|
"does not state), its have, each provision and its bus state; and the verb edges the graph leaves out on a cycle. Nodes " +
|
|
"the watchman says are offline are named away and left out. At most 500 modules, said when cut short; said when the " +
|
|
"module graph cannot be read. Give revision for an older one, none for the gate's newest. Read-only.",
|
|
Input: map[string]any{"type": "object", "properties": map[string]any{
|
|
"revision": map[string]any{"type": "integer", "description": "the source revision; none for the gate's newest"}}},
|
|
Run: func(args map[string]any) (any, error) { return r.TiersOf(args) }},
|
|
{Name: view.ModuleHistoryTool,
|
|
Description: "The history of one module's value in the indexer's modules state (modules.<module>), with times: each value it " +
|
|
"still keeps (up to 20), newest first, with its revision, when it was written, put or del, the repository and commit " +
|
|
"it was computed at, its source revision (src), its new-form fingerprint (fp2) or why it has none (none2), its manifest " +
|
|
"digest and path. Shows when a merge wrote a module, and when an older merge wrote it back. Read-only.",
|
|
Input: map[string]any{"type": "object", "required": []string{"module"}, "properties": map[string]any{
|
|
"module": map[string]any{"type": "string", "description": "the module's name"}}},
|
|
Run: func(args map[string]any) (any, error) {
|
|
name, _ := args["module"].(string)
|
|
return r.ModuleHistory(name)
|
|
}},
|
|
{Name: view.HeardTool,
|
|
Description: "The forge stream's (FORGE, the forge bridge's) recent messages as the indexer heard them: for each merged, pull, " +
|
|
"closed or open message it handled, newest first (its last 64), the subject, the stream sequence, how often the server " +
|
|
"delivered it (a redelivery shows above 1), when it was published and handled, its kind, repository, pull request, " +
|
|
"branch and commit (the merge commit, or the head), and what the indexer did: applied (with the modules written, or, " +
|
|
"for a late merged message, the later merge already taken in, when nothing was written), left for redelivery or dropped, with why. Read-only.",
|
|
Input: map[string]any{"type": "object", "properties": map[string]any{
|
|
"limit": map[string]any{"type": "integer", "description": "at most this many messages, newest first (default every one kept)"}}},
|
|
Run: func(args map[string]any) (any, error) {
|
|
limit, _ := args["limit"].(float64)
|
|
return r.Heard(int(limit))
|
|
}},
|
|
{Name: view.BuildUsageTool,
|
|
Description: "How the build nodes are used, and whether a node is eligible to build, per node (every node holding a build module or named by a capacity): each build seat it holds (node-build-go, -node, -image) with its capacity " +
|
|
"(mesh-config's resolved value, or the modules' default) and pause, and whether it is eligible to build and why not; the asks " +
|
|
"running now by kind (delivery, check, recheck, try), the keys left running (past the step bound, or their module not running) and the ones " +
|
|
"their module ended for that (a restart, or past the step bound), counted apart and in no median or occupancy; " +
|
|
"the steps started and finished in the last hour and the last day, failed ones, total and median time, by kind, and how long and " +
|
|
"what share of the window the node ran its capacity of asks. Mesh-wide: the backlog of asks waiting and held on each build seat's " +
|
|
"stream, and the tries running. Read from the bus's states (have, mesh-config, the build modules' steps and pauses, the checker's tries); " +
|
|
"the backlog from the view program's reading of the streams; CPU is not on the bus and the answer names the per-node tool. Read-only.",
|
|
Input: map[string]any{"type": "object", "properties": map[string]any{}},
|
|
Run: func(map[string]any) (any, error) { return u.Usage(), nil }},
|
|
{Name: view.ModuleReadTool,
|
|
Description: "What the indexer stored for one module under modules.<module>, as stored: its fingerprint (fp, none), the " +
|
|
"new-form fingerprint (fp2) or why it has none (none2), the inputs it was computed from (inputs2), the module's " +
|
|
"directory (path) and the toolchains it was computed with. Read-only.",
|
|
Input: map[string]any{"type": "object", "required": []string{"module"}, "properties": map[string]any{
|
|
"module": map[string]any{"type": "string", "description": "the module's name"}}},
|
|
Run: func(args map[string]any) (any, error) {
|
|
name, _ := args["module"].(string)
|
|
return r.Module(name)
|
|
}},
|
|
{Name: view.ModulesReadTool,
|
|
Description: "Every module the indexer holds, each as stored under modules.<module>. With none2 true, only the modules " +
|
|
"that have no new-form fingerprint, each with the reason (none2). Read-only.",
|
|
Input: map[string]any{"type": "object", "properties": map[string]any{
|
|
"none2": map[string]any{"type": "boolean", "description": "only the modules without a new-form fingerprint"}}},
|
|
Run: func(args map[string]any) (any, error) {
|
|
only, _ := args["none2"].(bool)
|
|
mods, err := r.Modules(only)
|
|
return map[string]any{"modules": mods}, err
|
|
}},
|
|
{Name: view.CandidateReadTool,
|
|
Description: "What the indexer stored for one candidate (the open pull requests of a branch) under candidates.<group>, as stored: " +
|
|
"the pull requests and heads, the base, and per pull request the touched modules with their new-form fingerprint (fp2) or why " +
|
|
"they have none (none2). Read-only.",
|
|
Input: map[string]any{"type": "object", "required": []string{"group"}, "properties": map[string]any{
|
|
"group": map[string]any{"type": "string", "description": "the candidate's group, the branch name its pull requests share"}}},
|
|
Run: func(args map[string]any) (any, error) {
|
|
group, _ := args["group"].(string)
|
|
return r.Candidate(group)
|
|
}},
|
|
{Name: view.PredefinedReadTool,
|
|
Description: "The catalogue's predefined list as the indexer keeps it (novox/hq ADR 0565 §2): its key list, the list whole as " +
|
|
"predefined.json holds it with the merge it was read at; a merge older than that never replaces it. mesh-config " +
|
|
"publishes the predefined keys from it after its take-over. Read-only.",
|
|
Run: func(map[string]any) (any, error) {
|
|
return view.ControlRecord(r.PredefinedState, view.IndexerPredefinedState, view.PredefinedListKey)
|
|
}},
|
|
{Name: view.ConfigCandidatesReadTool,
|
|
Description: "Each open pull request's config declarations as the indexer keeps them (novox/hq ADR 0565 §2): per pull " +
|
|
"request (owner.repository.number), its head and per module the config keys its manifests add or change against the " +
|
|
"trunk's, an empty set when none; deleted when it merges or closes. Give key for one. Read-only.",
|
|
Input: map[string]any{"type": "object", "properties": map[string]any{
|
|
"key": map[string]any{"type": "string", "description": "one pull request's key, owner.repository.number"}}},
|
|
Run: func(args map[string]any) (any, error) {
|
|
key, _ := args["key"].(string)
|
|
return view.ControlRecord(r.ConfigCandidatesState, view.IndexerConfigCandidatesState, key)
|
|
}},
|
|
{Name: view.PackagesReadTool,
|
|
Description: "The mesh packages as the indexer publishes them (novox/hq ADR 0593 §2): per mesh Go module (its module " +
|
|
"path, such as git.novox.be/novox/mesh-sdk/go), the trunk merge that published it (the version a build pins), the " +
|
|
"digest of its go.mod and go.sum, and the content digest of each package in it. A module's fingerprint takes the " +
|
|
"digests of the packages its last build imported, so this says why a package merge moved a module or not. Give " +
|
|
"module for one. Read-only.",
|
|
Input: map[string]any{"type": "object", "properties": map[string]any{
|
|
"module": map[string]any{"type": "string", "description": "one mesh Go module's path"}}},
|
|
Run: func(args map[string]any) (any, error) {
|
|
key := ""
|
|
if module, _ := args["module"].(string); strings.TrimSpace(module) != "" {
|
|
var err error
|
|
if key, err = view.PackagesKey(strings.TrimSpace(module)); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return view.ControlRecord(r.PackagesState, view.PackagesState, key)
|
|
}},
|
|
{Name: view.CandidatesReadTool,
|
|
Description: "Every candidate the indexer holds, by group, each as stored under candidates.<group>; with module, only those with a " +
|
|
"pull request touching that module. Keys that cannot be read are listed as unreadable. Read-only.",
|
|
Input: map[string]any{"type": "object", "properties": map[string]any{
|
|
"module": map[string]any{"type": "string", "description": "only candidates touching this module"}}},
|
|
Run: func(args map[string]any) (any, error) {
|
|
module, _ := args["module"].(string)
|
|
c, bad, err := r.Candidates(module)
|
|
return map[string]any{"candidates": c, "unreadable": bad}, err
|
|
}},
|
|
{Name: view.BuilderBuildsReadTool,
|
|
Description: "The builder's build keys, each as stored in mesh-delivery-builder_builds: key, module, fingerprint (src2) and build " +
|
|
"(state, by, step with the nodes it was built on, manifest, artifacts with their digests, toolchains, error). Narrow by module " +
|
|
"or state (queued, building, done, failed); at most limit builds (default 50), newest revision first for a module, and the answer says when it was cut and lists keys that are no build key (unreadable). Without a module, give one to get its newest. Read-only.",
|
|
Input: map[string]any{"type": "object", "properties": map[string]any{
|
|
"module": map[string]any{"type": "string", "description": "only this module's builds"},
|
|
"state": map[string]any{"type": "string", "description": "only builds in this state"},
|
|
"limit": map[string]any{"type": "integer", "description": "at most this many builds (default 50), newest revision first"}}},
|
|
Run: func(args map[string]any) (any, error) {
|
|
module, _ := args["module"].(string)
|
|
state, _ := args["state"].(string)
|
|
limit, _ := args["limit"].(float64)
|
|
return r.BuilderBuilds(module, state, int(limit))
|
|
}},
|
|
{Name: view.BuildHistoryTool,
|
|
Description: "Every build that ended since a time (default the last 24 hours), oldest first, from the builder's log build-log (kept 30 " +
|
|
"days): its module, fingerprint, repository, directory, commit and ref, the build node, when it ended, its outcome (done, failed or " +
|
|
"queued), the manifest, the bases it was built against, each artifact made, and seconds, how long it took from its start (absent " +
|
|
"when the start is older than since). With module, that module's builds only; more says the log held more: ask again from the " +
|
|
"last build's time. Read-only.",
|
|
Input: map[string]any{"type": "object", "properties": map[string]any{
|
|
"module": map[string]any{"type": "string", "description": "only this module's builds"},
|
|
"since": map[string]any{"type": "string", "description": "RFC 3339 time; default 24 hours ago"}}},
|
|
Run: func(args map[string]any) (any, error) {
|
|
module, _ := args["module"].(string)
|
|
since := time.Now().Add(-view.DefaultHistorySince)
|
|
if raw, _ := args["since"].(string); strings.TrimSpace(raw) != "" {
|
|
at, err := time.Parse(time.RFC3339Nano, strings.TrimSpace(raw))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("since %q is not an RFC 3339 time: %w", raw, err)
|
|
}
|
|
since = at
|
|
}
|
|
return r.BuildHistory(module, since)
|
|
}},
|
|
{Name: view.BuilderBuildReadTool,
|
|
Description: "One build of the builder as stored. Give the module and its fingerprint (fp2, src2:...), or the module alone for the build " +
|
|
"at the fingerprint the indexer holds for it now, joined through modules.<module>; with commit, only if the indexer holds the module at that commit (a module's value names the commit its fingerprint first appeared at, and stays there through merges that leave the module alone). Read-only.",
|
|
Input: map[string]any{"type": "object", "required": []string{"module"}, "properties": map[string]any{
|
|
"module": map[string]any{"type": "string", "description": "the module's name"},
|
|
"fp2": map[string]any{"type": "string", "description": "its new-form fingerprint; empty means the indexer's current one"},
|
|
"commit": map[string]any{"type": "string", "description": "the commit the indexer must hold the module at"}}},
|
|
Run: func(args map[string]any) (any, error) {
|
|
module, _ := args["module"].(string)
|
|
fp2, _ := args["fp2"].(string)
|
|
commit, _ := args["commit"].(string)
|
|
return r.BuilderBuild(module, fp2, commit)
|
|
}},
|
|
{Name: view.WantsReadTool,
|
|
Description: "The assigner's wants and holds as stored in mesh-delivery-assigner_wants: wants by key (want.<node>.<module>) and " +
|
|
"held fingerprints by key (held.<module>.<hex>). Narrow by node and module (a hold has no node and is shown when no node is given); a node and a module read that one key. " +
|
|
"At most 1000 values a call; the answer says when it was cut (cut, note). Read-only.",
|
|
Input: map[string]any{"type": "object", "properties": nodeModule},
|
|
Run: func(args map[string]any) (any, error) {
|
|
node, module := nodeModuleOf(args)
|
|
return r.Wants(node, module)
|
|
}},
|
|
{Name: view.WantReadTool,
|
|
Description: "One want of the assigner as stored: what the node is to run of the module (fingerprint, the newest it is held back from and the rule, removed, words). Read-only.",
|
|
Input: map[string]any{"type": "object", "required": []string{"node", "module"}, "properties": nodeModule},
|
|
Run: func(args map[string]any) (any, error) {
|
|
node, module := nodeModuleOf(args)
|
|
return r.Want(node, module)
|
|
}},
|
|
{Name: view.DifferencesReadTool,
|
|
Description: "The assigner's differences between a want and what the controller sends, as stored in mesh-delivery-assigner_differences, " +
|
|
"by key (<node>.<module>), with the rule that explains each or unexplained. Narrow by node and module (both: that one key). " +
|
|
"At most 1000 values a call; the answer says when it was cut (cut, note). Read-only.",
|
|
Input: map[string]any{"type": "object", "properties": nodeModule},
|
|
Run: func(args map[string]any) (any, error) {
|
|
node, module := nodeModuleOf(args)
|
|
return r.Differences(node, module)
|
|
}},
|
|
{Name: view.HavesReadTool,
|
|
Description: "What each node has, as the node-engine stored it in node-engine_have: by key <node>.<module> (the fingerprint it runs, its phase and health), " +
|
|
"and <node>.node (the node's own facts). Narrow by node and module (both: that one key). " +
|
|
"At most 1000 values a call; the answer says when it was cut (cut, note). Read-only.",
|
|
Input: map[string]any{"type": "object", "properties": nodeModule},
|
|
Run: func(args map[string]any) (any, error) {
|
|
node, module := nodeModuleOf(args)
|
|
return r.Haves(node, module)
|
|
}},
|
|
{Name: view.TrustsReadTool,
|
|
Description: "What every node trusts a mesh signature from, in one call (novox/hq ADR 0550 §2): each node's record's trusts " +
|
|
"(from node-engine_have <node>.node), whether it names the vault's signing key as mesh-vault publishes it in its " +
|
|
"state signers, the nodes that do not name the vault's yet (not_yet), and every: true once all do. Read-only.",
|
|
Input: map[string]any{"type": "object", "properties": map[string]any{}},
|
|
Run: func(map[string]any) (any, error) {
|
|
return r.Trusts()
|
|
}},
|
|
}
|
|
}
|
|
|
|
var nodeModule = map[string]any{
|
|
"node": map[string]any{"type": "string", "description": "only this node's keys"},
|
|
"module": map[string]any{"type": "string", "description": "only this module's keys"}}
|
|
|
|
func nodeModuleOf(args map[string]any) (string, string) {
|
|
node, _ := args["node"].(string)
|
|
module, _ := args["module"].(string)
|
|
return strings.TrimSpace(node), strings.TrimSpace(module)
|
|
}
|
|
|
|
// socket is the view program's socket, as the program reads it (MESH_DELIVERY_SOCKET, else its default).
|
|
func socket() string {
|
|
if s := strings.TrimSpace(os.Getenv("MESH_DELIVERY_SOCKET")); s != "" {
|
|
return s
|
|
}
|
|
return "/var/lib/mesh-delivery-view-cli/cli.sock"
|
|
}
|
|
|
|
// backlogOf asks the view program on its socket for the backlog it reads from the seats' streams' workers: the tool
|
|
// runtime grants a bundle states, and the stream and worker information is granted to the view program's own account
|
|
// (mesh-sdk buscompose.BuildBacklogReads).
|
|
func backlogOf(path string) func() ([]view.BacklogRow, error) {
|
|
return func() ([]view.BacklogRow, error) {
|
|
s, err := view.Ask(path)
|
|
return s.Backlog, err
|
|
}
|
|
}
|
|
|
|
func read(state stdio.KeptState) view.StateRead {
|
|
return view.StateRead{Keys: state.Keys, History: func(key string) ([]view.Revision, error) {
|
|
revs, err := state.History(key)
|
|
out := make([]view.Revision, 0, len(revs))
|
|
for _, rev := range revs {
|
|
out = append(out, view.Revision{Revision: rev.Revision, At: rev.At, Op: rev.Op, Value: rev.Value})
|
|
}
|
|
return out, err
|
|
}, GetEntry: func(key string) (json.RawMessage, uint64, error) {
|
|
e, err := state.Get(key)
|
|
if err != nil || e == nil {
|
|
return nil, 0, err
|
|
}
|
|
return e.Value, e.Revision, nil
|
|
}, Get: func(key string) (json.RawMessage, error) {
|
|
e, err := state.Get(key)
|
|
if err != nil || e == nil {
|
|
return nil, err
|
|
}
|
|
return e.Value, nil
|
|
}}
|
|
}
|
|
|
|
// ownersRead is the view's reading of the indexer's and the assigner's states: each through its owner's read tool, asked
|
|
// by the bundle on each call (ADR 0615 §1, design 25 § 12.2: the view keeps its tools and answers them by asking the
|
|
// owners), never their buckets.
|
|
func ownersRead(ask func(key string, body any) (json.RawMessage, error), catalog *meshcatalogclient.Branches) view.Reader {
|
|
// Each owner's read tool as the view's invokes names it.
|
|
indexer := func(tool string) view.StateRead { return view.OwnerRead(ask, view.IndexerModule+"."+tool) }
|
|
assigner := func(tool string) view.StateRead { return view.OwnerRead(ask, view.AssignerModule+"."+tool) }
|
|
return view.Reader{State: catalogRead(catalog), CandidatesState: indexer("indexer_read_candidates"),
|
|
WantsState: assigner("assigner_read_wants"), DifferencesState: assigner("assigner_read_differences"),
|
|
HeardState: indexer("indexer_read_heard"),
|
|
PredefinedState: indexer("indexer_read_predefined"),
|
|
ConfigCandidatesState: indexer("indexer_read_config_candidates"),
|
|
PackagesState: indexer("indexer_read_packages"),
|
|
GraphState: indexer("indexer_read_graph")}
|
|
}
|
|
|
|
// holderOf is a seat's holder as mesh-sdk's seatlist.HolderOf answers it (design 47 G A-126): false while the seat has none or
|
|
// the seat list cannot be read.
|
|
func holderOf(ask func(key string, body any) (json.RawMessage, error)) func(seat string) (string, bool) {
|
|
return func(seat string) (string, bool) {
|
|
module, err := seatlist.HolderOf(ask, seat)
|
|
return module, err == nil
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
logf := func(f string, a ...any) { fmt.Fprintf(os.Stderr, "[mesh-delivery-view-tools] "+f+"\n", a...) }
|
|
// The catalog half per watched branch, followed by the bundle itself through meshcatalogclient's branch follower on
|
|
// its own views reader (design 47 G §532, §614, A-103), as other bundles follow the views they read.
|
|
cat := &views.Reader{Module: viewstate.ViewModule, Logf: logf, Acts: meshhandactsclient.Recorder{Ask: stdio.Ask}}
|
|
branches, err := meshcatalogclient.FollowBranches(cat, stdio.Ask)
|
|
if err != nil {
|
|
logf("%v", err)
|
|
os.Exit(1)
|
|
}
|
|
branches.Logf = logf
|
|
cat.Start(context.Background())
|
|
r := ownersRead(stdio.Ask, branches)
|
|
r.Builds, r.HavesState = read(stdio.State("mesh-delivery-builder.builds")), read(stdio.State("node-engine.have"))
|
|
r.ReachState = read(stdio.State("reach"))
|
|
r.WaitsState = read(stdio.State(viewstate.WaitsState))
|
|
r.SignersState = read(stdio.State("mesh-vault.signers"))
|
|
r.ConditionsState, r.AttemptsState = read(stdio.State(view.StatusConditionsState)), read(stdio.State(view.StatusAttemptsState))
|
|
r.PresenceState, r.StuckState = read(stdio.State(view.StatusPresenceState)), read(stdio.State(view.StatusStuckState))
|
|
r.Sources = view.SourcesOf(stdio.Ask, branches.Followed)
|
|
r.Assignment, r.Difference = view.AssignmentOf(stdio.Ask), view.DifferenceOf(stdio.Ask)
|
|
r.ProvisionsState = read(stdio.State(view.ProvisionsRead))
|
|
r.BuildLog = stdio.Log(builds.BuildLogRead).Since
|
|
r.Pending = pendingOf
|
|
// The build modules are no names (design 47 G A-82): the build seats on the seat list, each held by the module the seat
|
|
// list names for it; the seat list and its two tools are wired once by mesh-sdk (§204).
|
|
// Its callers name seats, never a module, resolved from the seat-holders view the catalog half's claimant serves (A-295, A-298).
|
|
gate := seatlist.NewPart("view", []string{stdio.AnyPerson, stdio.CallerSeatPrefix + "node-tool-runner", stdio.CallerSeatPrefix + "mesh-healers"})
|
|
gate.Holders(branches.SeatHolders(context.Background()))
|
|
go func() {
|
|
<-stdio.Served()
|
|
gate.Follow(seatlist.StdioBus(), nil, func(f string, a ...any) { fmt.Fprintf(os.Stderr, "[mesh-delivery-view-tools] "+f+"\n", a...) })
|
|
}()
|
|
seats := buildrun.NewBuildSeats(func() map[string]seatdef.Definition {
|
|
if rd := gate.Reader(); rd != nil {
|
|
return rd.Definitions()
|
|
}
|
|
return nil
|
|
}, holderOf(stdio.Ask))
|
|
u := view.UsageReader{Haves: r.HavesState, Resolved: read(stdio.State("mesh-config.resolved")), Tries: read(stdio.State("mesh-delivery-checker.tries")),
|
|
Seats: seats,
|
|
StepsOf: func(seat string) view.StateRead { return read(stdio.State(seat + "." + view.StepsState)) },
|
|
PausesOf: func(seat string) view.StateRead { return read(stdio.State(seat + "." + buildstep.PausesState)) },
|
|
Backlog: backlogOf(socket())}
|
|
// view_first_views and view_first_view_again: where the view's process stands with its first views of the indexer's and
|
|
// the assigner's states (ADR 0615 §6), and a re-read by hand.
|
|
tools := append(Tools(r, u), gate.Tools()...)
|
|
tools = append(tools, AnswersReadTool(func(name string) views.Source { return views.KeptSource{State: stdio.State(name)} }, time.Now))
|
|
tools = append(tools, views.StatusTools(viewstate.ViewModule, "view_first_views", "view_first_view_again", stdio.State(views.StatusState), meshhandactsclient.Recorder{Ask: stdio.Ask}, time.Now)...)
|
|
if err := stdio.Serve("", tools); err != nil {
|
|
fmt.Fprintf(os.Stderr, "[mesh-delivery-view-tools] %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// AnswersReadTool is view_read_answers, the view's ordinary read tool of its own state answers (mesh-sdk views.ReadTool): the
|
|
// teller's first view of the answers the view says on views.answers (design 47 G A-175, A-178).
|
|
func AnswersReadTool(state func(string) views.Source, now func() time.Time) stdio.Tool {
|
|
return views.ReadTool(viewstate.ViewModule, viewstate.ViewAnswersReadTool, viewstate.ViewAnswersState,
|
|
"the operator's answers given in the view to the assigner's asks, one key <ask key>.view each, which the teller fills the ask from.",
|
|
state(viewstate.ViewAnswersState), state(views.MarksState), now)
|
|
}
|
|
|
|
// pendingOf asks the indexer's tool indexer_pending (declared in invokes): what it holds back and why (ADR 0633 §5).
|
|
func pendingOf() (takein.PendingAnswer, error) {
|
|
raw, err := stdio.Ask(view.IndexerModule+".indexer_pending", map[string]any{})
|
|
if err != nil {
|
|
return takein.PendingAnswer{}, err
|
|
}
|
|
return takein.DecodePendingAnswer(raw)
|
|
}
|