Tries let in while waiting may later all run at once; each node's build capacity bounds what runs on it (review of issue 1890's try cap).
285 lines
15 KiB
Go
285 lines
15 KiB
Go
// Command mesh-delivery-checker-tools serves the checker's read tools (novox/hq ADR 0484 clause 13, issue 1686): the runtime
|
|
// launches it for a call, it reads the checker's verdicts and kept states and the indexer's candidates through the checker's
|
|
// state and reads, and writes nothing. The checker is the other program: it serves no tool (ADR 0437 §4).
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery/buildrun"
|
|
|
|
"mesh-delivery-checker/internal/check"
|
|
)
|
|
|
|
// reads counts the values this call read from the bus: the runtime launches the tools for one call, so the count is the call's.
|
|
var reads atomic.Int64
|
|
|
|
// cutAnswer says in the answer that it was cut, and how to narrow it.
|
|
func cutAnswer(answer map[string]any, cut string) map[string]any {
|
|
if cut != "" {
|
|
answer["cut"], answer["note"] = true, cut
|
|
}
|
|
return answer
|
|
}
|
|
|
|
// logged runs every tool with one line to stderr per call: the tool, the values it read and the time it took.
|
|
func logged(tools []stdio.Tool) []stdio.Tool {
|
|
for i := range tools {
|
|
name, run := tools[i].Name, tools[i].Run
|
|
tools[i].Run = func(args map[string]any) (any, error) {
|
|
reads.Store(0)
|
|
start := time.Now()
|
|
out, err := run(args)
|
|
fmt.Fprintf(os.Stderr, "[mesh-delivery-checker-tools] %s read %d values in %s (error: %v)\n", name, reads.Load(), time.Since(start).Round(time.Millisecond), err)
|
|
return out, err
|
|
}
|
|
}
|
|
return tools
|
|
}
|
|
|
|
func read(s stdio.KeptState) check.StateRead {
|
|
return check.StateRead{Keys: s.Keys, Get: func(key string) (json.RawMessage, error) {
|
|
reads.Add(1)
|
|
e, err := s.Get(key)
|
|
if err != nil || e == nil {
|
|
return nil, err
|
|
}
|
|
return e.Value, nil
|
|
}}
|
|
}
|
|
|
|
// runsOf says which commit the mesh runs of a module from the indexer's modules state, as the checker's own record of it does
|
|
// (the commit the module's record was computed at); "" when the record is not readable.
|
|
type runsOf struct{ modules check.StateRead }
|
|
|
|
func (r runsOf) CommitOf(module string) string {
|
|
if r.modules.Get == nil {
|
|
return ""
|
|
}
|
|
raw, err := r.modules.Get(module)
|
|
if err != nil || len(raw) == 0 {
|
|
return ""
|
|
}
|
|
m, err := delivery.DecodeModule(raw)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return m.Commit
|
|
}
|
|
|
|
// stateStore is a state the tools write: the tries.
|
|
type stateStore struct {
|
|
check.StateRead
|
|
create func(key string, value []byte) error
|
|
entry func(key string) (json.RawMessage, uint64, error)
|
|
update func(key string, value []byte, revision uint64) error
|
|
}
|
|
|
|
func (s stateStore) Entry(key string) (json.RawMessage, uint64, error) { return s.entry(key) }
|
|
func (s stateStore) Update(key string, value []byte, revision uint64) error {
|
|
return s.update(key, value, revision)
|
|
}
|
|
|
|
func (s stateStore) Keys() ([]string, error) { return s.StateRead.Keys() }
|
|
func (s stateStore) Get(key string) (json.RawMessage, error) { return s.StateRead.Get(key) }
|
|
func (s stateStore) Create(key string, value []byte) error { return s.create(key, value) }
|
|
|
|
// Trying is what the try's tools work on: the tries, and the gate's trunk heads.
|
|
type Trying struct {
|
|
Tries check.TryStore
|
|
Ender check.TryEnder
|
|
Trunks func() (map[string]string, error)
|
|
Now func() time.Time
|
|
}
|
|
|
|
// TryTools are the try's tools (ADR 0501 §5): ask one, read one, and end one by hand (issue 734). mesh-controller's try calls them.
|
|
func TryTools(tr Trying) []stdio.Tool {
|
|
str := func(args map[string]any, k string) string { s, _ := args[k].(string); return s }
|
|
return []stdio.Tool{
|
|
{Name: check.TryTool,
|
|
Description: "Try a pushed head before its pull request is opened: the checker judges it as it judges a pull request's head, merged onto its trunk's head, " +
|
|
"its unit and integration levels through the build modules, and owes the forge nothing. Answers the try's id at once; " + check.TryReadTool + " reads " +
|
|
"where it stands. A try of the same head that waits or runs is the answer to a second ask; a new try is refused while 4 tries run on a build node or 8 wait for one, mesh-wide; tries let in while waiting may later all run at once, and each node's build capacity bounds what runs on it. A repository " +
|
|
"the checker checks no level of (one whose trunk the gate does not name) is refused. A try whose ask no build node took within the step " +
|
|
"bound ends as error; its ask stays in the build seat's stream and may still run later, judged by nobody (issue 734).",
|
|
Input: map[string]any{"type": "object", "required": []string{"repository", "head"}, "properties": map[string]any{
|
|
"repository": map[string]any{"type": "string", "description": "<owner>/<name>"},
|
|
"head": map[string]any{"type": "string", "description": "the full commit to try, 40 or 64 hex digits"},
|
|
"trunk": map[string]any{"type": "string", "description": "the trunk commit to merge it onto, or the trunk's branch for its head now; the trunk's head when absent"},
|
|
"tests": map[string]any{"type": "string", "description": "a go test -run pattern: only those tests run, and nothing is kept"},
|
|
"level": map[string]any{"type": "string", "enum": []string{"unit", "integration"}, "description": "only this level; both when absent"}}},
|
|
Run: func(args map[string]any) (any, error) {
|
|
if tr.Tries == nil || tr.Trunks == nil {
|
|
return nil, fmt.Errorf("the tries state cannot be written here")
|
|
}
|
|
trunks, err := tr.Trunks()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the trunk heads cannot be read: %w", err)
|
|
}
|
|
now := time.Now
|
|
if tr.Now != nil {
|
|
now = tr.Now
|
|
}
|
|
id, err := check.AskTry(tr.Tries, trunks, check.TryAsk{Repository: str(args, "repository"), Head: str(args, "head"),
|
|
Trunk: str(args, "trunk"), Tests: str(args, "tests"), Level: str(args, "level")}, now())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return map[string]any{"id": id}, nil
|
|
}},
|
|
{Name: check.TryReadTool,
|
|
Description: "Where a try stands, as stored: queued, running, or its end (pass, fail, or error when it could not run), with what was said of it, " +
|
|
"the head and the trunk commit it was merged onto, the time it took and what failed first; while it runs, each ask it put as soon as it is put, " +
|
|
"with the node that took it, its state and the phase its step says (fetch and merge, a level's digest, lint, tests) and since when. Read-only.",
|
|
Input: map[string]any{"type": "object", "required": []string{"id"}, "properties": map[string]any{
|
|
"id": map[string]any{"type": "string", "description": "the try's id, try-<digits>"}}},
|
|
Run: func(args map[string]any) (any, error) {
|
|
if tr.Tries == nil {
|
|
return nil, fmt.Errorf("the tries state cannot be read here")
|
|
}
|
|
return check.ReadTry(tr.Tries, str(args, "id"))
|
|
}},
|
|
{Name: check.TryEndTool,
|
|
Description: "End a try that waits or runs, as error, by hand: say why (kept in what it says). Its place among the 4 is free at once, " +
|
|
"the checker stops waiting for its asks, and the next " + check.TryTool + " of its head asks anew with a new id. For a try whose ask " +
|
|
"was lost (no step runs for it in checker_status); the checker ends one by itself when the build seat gives its ask up, or no node " +
|
|
"takes it within the step bound. A try that ended already is refused.",
|
|
Input: map[string]any{"type": "object", "required": []string{"id", "why"}, "properties": map[string]any{
|
|
"id": map[string]any{"type": "string", "description": "the try's id, try-<digits>"},
|
|
"why": map[string]any{"type": "string", "description": "why it is ended, in words"}}},
|
|
Run: func(args map[string]any) (any, error) {
|
|
if tr.Ender == nil {
|
|
return nil, fmt.Errorf("the tries state cannot be written here")
|
|
}
|
|
now := time.Now
|
|
if tr.Now != nil {
|
|
now = tr.Now
|
|
}
|
|
t, err := check.EndTry(tr.Ender, str(args, "id"), str(args, "why"), now())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return map[string]any{"id": t.ID, "state": t.State, "said": t.Said}, nil
|
|
}},
|
|
}
|
|
}
|
|
|
|
// Tools are the read tools over the checker's states.
|
|
func Tools(r check.Reader) []stdio.Tool {
|
|
return []stdio.Tool{
|
|
{Name: check.VerdictsReadTool,
|
|
Description: "The verdicts the checker holds, each as stored under verdicts.<group>.<heads>: its state, what failed first, the tests run, kept and skipped per level, the time of each part, the trunk heads it judged against and the composition, with the full " +
|
|
"report of the status each pull request carries (the line the forge shows is cut at 140 characters, this is not). With a group, that group's only. Read-only.",
|
|
Input: map[string]any{"type": "object", "properties": map[string]any{
|
|
"group": map[string]any{"type": "string", "description": "the delivery group's branch name"}}},
|
|
Run: func(args map[string]any) (any, error) {
|
|
group, _ := args["group"].(string)
|
|
vs, cut, err := r.Verdicts(group)
|
|
return cutAnswer(map[string]any{"verdicts": vs}, cut), err
|
|
}},
|
|
{Name: check.PullReadTool,
|
|
Description: "The verdict that judges one pull request and the full report of its status mesh/check. Read-only.",
|
|
Input: map[string]any{"type": "object", "required": []string{"repository", "number"}, "properties": map[string]any{
|
|
"repository": map[string]any{"type": "string", "description": "<owner>/<name>"},
|
|
"number": map[string]any{"type": "integer", "description": "the pull request's number"}}},
|
|
Run: func(args map[string]any) (any, error) {
|
|
repository, _ := args["repository"].(string)
|
|
n, _ := args["number"].(float64)
|
|
return r.Pull(repository, int(n))
|
|
}},
|
|
{Name: check.KeptReadTool,
|
|
Description: "The kept verdicts of passing packages, each as stored under kept.<level>.<digest>: the package, its tests, the time they took, the toolchain " +
|
|
"and when it was kept. With a level, that level's; with a level and a digest, that one. Read-only.",
|
|
Input: map[string]any{"type": "object", "properties": map[string]any{
|
|
"level": map[string]any{"type": "string", "enum": []string{"unit", "integration"}, "description": "the level"},
|
|
"digest": map[string]any{"type": "string", "description": "the package's digest, with a level"}}},
|
|
Run: func(args map[string]any) (any, error) {
|
|
level, _ := args["level"].(string)
|
|
digest, _ := args["digest"].(string)
|
|
kept, cut, err := r.Kept(level, digest)
|
|
return cutAnswer(map[string]any{"kept": kept}, cut), err
|
|
}},
|
|
{Name: check.PreviewsReadTool,
|
|
Description: "The merge previews the checker holds, each as stored under previews.<repository>.<head>.<trunk>: the merge commit, per module directory " +
|
|
"its tree, manifest digest and lock hashes, or the files in conflict. With a repository, that repository's; with a head (7 or more characters) " +
|
|
"only that head's. Keys that are no preview key are listed as unreadable. Read-only.",
|
|
Input: map[string]any{"type": "object", "properties": map[string]any{
|
|
"repository": map[string]any{"type": "string", "description": "<owner>/<name>"},
|
|
"head": map[string]any{"type": "string", "description": "a pull request head commit, or a prefix of at least 7 characters"}}},
|
|
Run: func(args map[string]any) (any, error) {
|
|
repository, _ := args["repository"].(string)
|
|
head, _ := args["head"].(string)
|
|
if head != "" && len(head) < 7 {
|
|
return nil, fmt.Errorf("the head %q is too short: give at least 7 characters", head)
|
|
}
|
|
ps, bad, cut, err := r.Previews(repository, head)
|
|
return cutAnswer(map[string]any{"previews": ps, "unreadable": bad}, cut), err
|
|
}},
|
|
{Name: check.StatusReadTool,
|
|
Description: "What runs: the steps in flight on the build modules by node, the candidates without a verdict for their heads, " +
|
|
"and the candidates whose check is owed by kind (waiting_by_kind: new, nodes, and trunk, a re-judge pulled after the other two). A key a " +
|
|
"build module left running (started past the step bound, or its build module not running on its node) is listed apart under " +
|
|
"left_running with why, and not counted. Nothing holds the checker and it has no capacity of its own: a check is an ask on the " +
|
|
"build seats' streams, whose backlog the delivery view shows (novox/hq ADR 0525). held_by_turn names the candidates whose check an approved " +
|
|
"merge's turn holds off (novox/hq ADR 0548 §3), with the repository and the merge that waits; the gate's turns tool reads every turn and its " +
|
|
"hand-now tool steers one. dead_letters says whether the checker hears the asks the build seats give up on (issue 734), and why not: " +
|
|
"deaf, a try or check whose ask is given up waits until its bound. Read-only.",
|
|
Input: map[string]any{"type": "object", "properties": map[string]any{}},
|
|
Run: func(map[string]any) (any, error) { return r.Status() }},
|
|
}
|
|
}
|
|
|
|
// stepBound is the checker's step bound as the checker reads it (MESH_CHECKER_STEP_BOUND), its default when unset or not a
|
|
// duration.
|
|
func stepBound() time.Duration {
|
|
if d, err := time.ParseDuration(os.Getenv("MESH_CHECKER_STEP_BOUND")); err == nil && d > 0 {
|
|
return d
|
|
}
|
|
return check.DefaultStepBound
|
|
}
|
|
|
|
func main() {
|
|
r := check.Reader{VerdictState: read(stdio.State("verdicts")), KeptState: read(stdio.State("kept")), CandidateState: read(stdio.State("mesh-delivery-indexer.candidates")),
|
|
PreviewState: read(stdio.State("previews")),
|
|
StepStates: map[string]check.StateRead{"mesh-build-go": read(stdio.State("mesh-build-go.steps")), "mesh-build-node": read(stdio.State("mesh-build-node.steps"))},
|
|
HaveState: read(stdio.State("node-engine.have")), StepBound: stepBound(),
|
|
TurnState: read(stdio.State("mesh-delivery-gate.turns")), HearingState: read(stdio.State(buildrun.HearingState))}
|
|
r.Runs = runsOf{read(stdio.State("mesh-delivery-indexer.modules"))} // the node-engine at the commit the mesh runs, as the checker reads it
|
|
tries := stdio.State(check.TriesState)
|
|
source := stdio.State("mesh-delivery-gate.source")
|
|
trunks := func() (map[string]string, error) {
|
|
e, err := source.Get(delivery.SourceMainKey)
|
|
if err != nil || e == nil {
|
|
return nil, err
|
|
}
|
|
s, err := delivery.DecodeSource(e.Value)
|
|
return s.Heads, err
|
|
}
|
|
r.Trunks = trunks
|
|
store := stateStore{StateRead: read(tries), create: func(key string, value []byte) error {
|
|
_, err := tries.Create(key, json.RawMessage(value))
|
|
return err
|
|
}, entry: func(key string) (json.RawMessage, uint64, error) {
|
|
reads.Add(1)
|
|
e, err := tries.Get(key)
|
|
if err != nil || e == nil {
|
|
return nil, 0, err
|
|
}
|
|
return e.Value, e.Revision, nil
|
|
}, update: func(key string, value []byte, revision uint64) error {
|
|
_, err := tries.Update(key, json.RawMessage(value), revision)
|
|
return err
|
|
}}
|
|
trying := Trying{Tries: store, Ender: store, Trunks: trunks}
|
|
if err := stdio.Serve("", logged(append(Tools(r), TryTools(trying)...))); err != nil {
|
|
fmt.Fprintf(os.Stderr, "[mesh-delivery-checker-tools] %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|