The controller asks no build of anybody (ADR 0501 §5, ADR 0525 clause 6, ADR 0503 §3). Every module of a walk is taken from the builder's done key; a key not done yet waits, named in the plan, and never fails it. - C2b/C2c: internal/builder and cmd/mesh-controller's asks go (askABuild, onlyTheBuildersOwn, buildOneAsked, takeIn from an outcome, buildAgentsOwn, the builder's say and its fallback, the daemon hearing an outcome). The server follows the retired seats' built subjects only to let go of them, because the installer's carried user list names them. - C2d: replay, queue, cancel, clear, kill, pause, resume, delivery-check, check-here and merge-gate are refused by name. - C3: the check queues (CHECKS_READY, CHECKS_OTHER) and their workers, the checks-running and plans-building buckets and their grants, the build queue and its hold (plans_building.go, queue.go, link/holds.go, builds_nats.go, checks_nats.go). - C4: merge_gate*.go, the pull request's repo-check path, the Checked event and its layers; a pull request asks the controller nothing but to tell mesh-config what its head declares. - C5: the builds bucket is written only by key_takein. - C10 (help and refusals): the terminal's help lists only what is served. Integration tests of the plan's tiers, sends, stops and retries now run on a stand-in builder with a done key for every module (walk_keys_scaffold_integration_test.go); the tests of the asks, the queue, the merge gate and the replays are deleted.
149 lines
5.0 KiB
Go
149 lines
5.0 KiB
Go
package main
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"flag"
|
||
"fmt"
|
||
"os"
|
||
"slices"
|
||
"sort"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/novox/mesh-controller/internal/inventory"
|
||
)
|
||
|
||
// What the core's bounds are set from (novox/hq to-be 45 Phase 0).
|
||
//
|
||
// **A bound is set from what was measured, not from what seemed reasonable.** Phase 1 puts a watchdog
|
||
// on each row of the signals table, and each has a bound: S1 three heartbeat intervals, S2 three times
|
||
// a machine's last apply, S3 a tier's build and apply time, S6 a build's timeout. Marked provisional
|
||
// in the design until a fortnight of these says what the mesh actually takes. Recorded by the serving
|
||
// controller as it hears each — a send's first report, a machine's next word, a plan leaving a tier, a
|
||
// build's outcome — and summarised here per machine, repository or module.
|
||
|
||
// durationSummary is one subject's measurements of one kind.
|
||
type durationSummary struct {
|
||
Kind string `json:"kind"`
|
||
Subject string `json:"subject"`
|
||
Count int `json:"count"`
|
||
Median string `json:"median"`
|
||
P90 string `json:"p90"`
|
||
Max string `json:"max"`
|
||
// Bound is what to-be 45's rule would make of these, where the rule is a multiple of a measured
|
||
// time: three times the slowest apply (S2), three times the median word interval (S1).
|
||
Suggests string `json:"suggests,omitempty"`
|
||
}
|
||
|
||
func summarise(ds []inventory.Duration) []durationSummary {
|
||
type key struct{ kind, subject string }
|
||
by := map[key][]time.Duration{}
|
||
for _, d := range ds {
|
||
k := key{d.Kind, d.Subject}
|
||
by[k] = append(by[k], d.Took)
|
||
}
|
||
var out []durationSummary
|
||
for k, took := range by {
|
||
slices.Sort(took)
|
||
at := func(q float64) time.Duration { return took[int(q*float64(len(took)-1))] }
|
||
s := durationSummary{Kind: k.kind, Subject: k.subject, Count: len(took),
|
||
Median: round(at(0.5)), P90: round(at(0.9)), Max: round(took[len(took)-1])}
|
||
switch k.kind {
|
||
case inventory.DurationApply:
|
||
s.Suggests = "S2 bound max(2m, 3×last apply) ≈ " + round(max(2*time.Minute, 3*at(0.9))) + " at the p90"
|
||
}
|
||
out = append(out, s)
|
||
}
|
||
sort.Slice(out, func(i, j int) bool {
|
||
ki, kj := slices.Index(inventory.DurationKinds, out[i].Kind), slices.Index(inventory.DurationKinds, out[j].Kind)
|
||
if ki != kj {
|
||
return ki < kj
|
||
}
|
||
return out[i].Subject < out[j].Subject
|
||
})
|
||
return out
|
||
}
|
||
|
||
func round(d time.Duration) string {
|
||
switch {
|
||
case d < time.Second:
|
||
return d.Round(time.Millisecond).String()
|
||
case d < time.Minute:
|
||
return d.Round(100 * time.Millisecond).String()
|
||
default:
|
||
return d.Round(time.Second).String()
|
||
}
|
||
}
|
||
|
||
// durationsCommand is `durations`: the summary per kind and subject, or every measurement as data.
|
||
func durationsCommand(ctx context.Context, args []string) error {
|
||
set := flag.NewFlagSet("durations", flag.ContinueOnError)
|
||
kind := set.String("kind", "", "one kind: "+strings.Join(inventory.DurationKinds, ", "))
|
||
days := set.Int("days", 14, "how many days back")
|
||
asJSON := set.Bool("json", false, "the summary as data")
|
||
all := set.Bool("all", false, "every measurement rather than the summary")
|
||
if _, err := parseAround(set, args); err != nil {
|
||
return err
|
||
}
|
||
if *kind != "" && !slices.Contains(inventory.DurationKinds, *kind) {
|
||
return fmt.Errorf("%q is not a kind of duration: %s", *kind, strings.Join(inventory.DurationKinds, ", "))
|
||
}
|
||
open, err := openStores(ctx)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer open.Close()
|
||
ds, err := open.inventory.Durations(ctx, *kind, time.Now().Add(-time.Duration(*days)*24*time.Hour))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if *all {
|
||
body, err := json.MarshalIndent(ds, "", " ")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
fmt.Println(string(body))
|
||
return nil
|
||
}
|
||
summary := summarise(ds)
|
||
if *asJSON {
|
||
body, err := json.MarshalIndent(map[string]any{"days": *days, "durations": summary}, "", " ")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
fmt.Println(string(body))
|
||
return nil
|
||
}
|
||
if len(summary) == 0 {
|
||
fmt.Printf("nothing measured in the last %d day(s): the serving controller records apply, "+
|
||
"plan-tier and build durations as it hears them\n", *days)
|
||
return nil
|
||
}
|
||
fmt.Printf("durations over the last %d day(s) — what the core's bounds are set from (to-be 45 Phase 0)\n\n", *days)
|
||
fmt.Printf(" %-14s %-28s %6s %10s %10s %10s\n", "kind", "of", "count", "median", "p90", "max")
|
||
for _, s := range summary {
|
||
fmt.Printf(" %-14s %-28s %6d %10s %10s %10s\n", s.Kind, s.Subject, s.Count, s.Median, s.P90, s.Max)
|
||
if s.Suggests != "" {
|
||
fmt.Printf(" %-14s %-28s %s\n", "", "", s.Suggests)
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// forgettingOldDurations removes what is older than a month, at start and daily after.
|
||
func forgettingOldDurations(ctx context.Context, inv *inventory.Inventory) {
|
||
for {
|
||
if n, err := inv.ForgetOldDurations(ctx); err != nil {
|
||
fmt.Fprintf(os.Stderr, "durations older than %s could not be removed: %v\n", inventory.DurationsKeptFor, err)
|
||
} else if n > 0 {
|
||
fmt.Printf("removed %d duration(s) older than %s\n", n, inventory.DurationsKeptFor)
|
||
}
|
||
select {
|
||
case <-ctx.Done():
|
||
return
|
||
case <-time.After(24 * time.Hour):
|
||
}
|
||
}
|
||
}
|