mesh/delivery merged: source.main moved to novox/mesh-delivery@2ac146078f47
mesh/check being carried to 726bbf1f1ffa on 2ac146078f47 | from e753009fbc71
The view remembered a reach value, a reached head and a motion event as written or said before the bus took them; a write refused at its start (no responders) was never made again while the derivation stayed the same, and mesh-delivery.show said mesh-delivery-view not running on shanks while node_haves said it running. Quick fix: forget only the refused reach write. Thorough fix, chosen: every write and event the view makes after its lock (reach, reached head, delivered revision, motion) answers its error, and the model forgets what was refused, so the next change it takes in makes it again; one place, the writer of the state both mesh-delivery.show and knowledge-base.progress read.
349 lines
12 KiB
Go
349 lines
12 KiB
Go
package view
|
|
|
|
import (
|
|
"fmt"
|
|
"maps"
|
|
"slices"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// How far each repository reached (novox/hq ADR 0556 clauses 2 and 3, issue 1280). The view keeps, per repository the gate's
|
|
// source names, how far each of its modules reached on the nodes that want it, and derives the repository's reached head: the
|
|
// newest head of the repository in the source's history, not past the merge the indexer took in, at which every wanted module's
|
|
// floor is at or after its last change at or before that head. It writes that as its state reach and says each new reached
|
|
// head as its event delivered, per repository. Nothing here records a merge as delivered (ADR 0429 §1): a merge at or before
|
|
// the reached head is on the machines, derived by whoever reads it.
|
|
|
|
// ReachHistory names the watch of a bucket with its history, as Initial is told it: the modules' and the source's history
|
|
// give each fingerprint's first commit and each head's source revision.
|
|
func ReachHistory(bucket string) string { return bucket + "#history" }
|
|
|
|
// reachBuckets are the watches the derivation waits for before it writes or says anything, so a start does not write a reach
|
|
// derived from half its inputs.
|
|
var reachBuckets = []string{delivery.ModulesBucket, delivery.AssignerWantsBucket, delivery.HaveBucket, delivery.SourceBucket,
|
|
delivery.TakenBucket, ReachHistory(delivery.ModulesBucket), ReachHistory(delivery.SourceBucket)}
|
|
|
|
// ReachWrite is a reach value to write under its key in the view's state reach.
|
|
type ReachWrite struct {
|
|
Key string
|
|
Value []byte
|
|
}
|
|
|
|
// SetReachWriter sets where the view's reach values go; nothing is written until it is set. A value it answers an error for is
|
|
// written again at the next change the view takes in (sendReach).
|
|
func (m *Model) SetReachWriter(put func(ReachWrite) error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.reachPut = put
|
|
}
|
|
|
|
// applyHistory takes one revision of a watch with history: of the indexer's modules (each module's values in order) and of
|
|
// the gate's source (each revision's heads).
|
|
func (m *Model) applyHistory(e Entry) {
|
|
switch e.Bucket {
|
|
case delivery.ModulesBucket:
|
|
if e.Deleted {
|
|
delete(m.modHist, e.Key)
|
|
return
|
|
}
|
|
mod, err := delivery.DecodeModule(e.Value)
|
|
if err != nil || mod.FP2 == "" || mod.Commit == "" {
|
|
return
|
|
}
|
|
h := m.modHist[e.Key]
|
|
if n := len(h); n > 0 && h[n-1].FP2 == mod.FP2 && h[n-1].Commit == mod.Commit {
|
|
if mod.Src != 0 {
|
|
h[n-1].Src = mod.Src
|
|
}
|
|
return
|
|
}
|
|
h = append(h, mod)
|
|
if len(h) > 2*delivery.MostChanges {
|
|
h = h[len(h)-delivery.MostChanges:]
|
|
}
|
|
m.modHist[e.Key] = h
|
|
case delivery.SourceBucket:
|
|
if e.Key != delivery.SourceMainKey || e.Deleted || e.Revision == 0 {
|
|
return
|
|
}
|
|
src, err := delivery.DecodeSource(e.Value)
|
|
if err != nil {
|
|
return
|
|
}
|
|
m.srcHist[e.Revision] = src.Heads
|
|
}
|
|
}
|
|
|
|
// headAt is one head of a repository and the first source revision that named it.
|
|
type headAt struct {
|
|
commit string
|
|
rev uint64
|
|
}
|
|
|
|
// headsOf are the heads of a repository in the source's history, oldest first, each with the first revision naming it.
|
|
func (m *Model) headsOf(repository string) []headAt {
|
|
var out []headAt
|
|
for _, rev := range slices.Sorted(maps.Keys(m.srcHist)) {
|
|
for r, head := range m.srcHist[rev] {
|
|
if !delivery.SameRepository(r, repository) || head == "" {
|
|
continue
|
|
}
|
|
if n := len(out); n == 0 || !sameCommit(out[n-1].commit, head) {
|
|
out = append(out, headAt{commit: head, rev: rev})
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// revOf is the first source revision naming a commit as the repository's head, or 0 when the history holds none.
|
|
func revOf(heads []headAt, commit string) uint64 {
|
|
for _, h := range heads {
|
|
if sameCommit(h.commit, commit) {
|
|
return h.rev
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// sameCommit is whether two spellings of a commit are one (either may be abbreviated to at least seven characters).
|
|
func sameCommit(a, b string) bool {
|
|
a, b = strings.ToLower(a), strings.ToLower(b)
|
|
if len(a) > len(b) {
|
|
a, b = b, a
|
|
}
|
|
return len(a) >= 7 && strings.HasPrefix(b, a)
|
|
}
|
|
|
|
// noBound is a module's bound when its floor is its newest change: it holds no head back.
|
|
const noBound = ^uint64(0)
|
|
|
|
// wantKeys are the wants' keys by module, each sorted: read once for every repository a derivation looks at.
|
|
func (m *Model) wantKeys() map[string][]string {
|
|
out := map[string][]string{}
|
|
for _, key := range slices.Sorted(maps.Keys(m.wants)) {
|
|
_, module := splitKey(key)
|
|
out[module] = append(out[module], key)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// deriveReach is the reach of one repository from the model as it stands; wants are the wants' keys by module (wantKeys).
|
|
func (m *Model) deriveReach(repository string, wants map[string][]string) delivery.Reach {
|
|
r := delivery.Reach{Repository: delivery.RepositoryName(repository), Modules: map[string]delivery.ModuleReach{}}
|
|
heads := m.headsOf(repository)
|
|
key, _ := delivery.RepositoryKey(repository)
|
|
if t, ok := m.taken[key]; ok {
|
|
r.Indexed = t.Merge
|
|
}
|
|
bound := noBound
|
|
var holds []string
|
|
for _, name := range slices.Sorted(maps.Keys(m.modules)) {
|
|
me := m.modules[name]
|
|
if me.err != "" || me.value.FP2 == "" || !delivery.SameRepository(me.value.Repository, repository) {
|
|
continue
|
|
}
|
|
mr, b, words := m.moduleReach(name, me.value, heads, wants[name])
|
|
r.Modules[name] = mr
|
|
if b < bound {
|
|
bound = b
|
|
}
|
|
if b != noBound {
|
|
holds = append(holds, words)
|
|
}
|
|
}
|
|
if len(r.Modules) == 0 {
|
|
r.Modules = nil
|
|
}
|
|
indexed := uint64(0)
|
|
if r.Indexed != "" {
|
|
indexed = revOf(heads, r.Indexed)
|
|
}
|
|
switch {
|
|
case r.Indexed == "":
|
|
holds = append(holds, "the indexer has taken in no merge of "+r.Repository)
|
|
case indexed == 0:
|
|
holds = append(holds, fmt.Sprintf("the merge the indexer took in, %.12s, is not a head of %s in the source's history", r.Indexed, r.Repository))
|
|
}
|
|
for _, h := range heads {
|
|
if h.rev <= indexed && h.rev < bound {
|
|
r.Reached, r.ReachedSrc = h.commit, h.rev
|
|
}
|
|
}
|
|
if n := len(heads); n > 0 && !sameCommit(heads[n-1].commit, r.Reached) {
|
|
if indexed != 0 && indexed < heads[n-1].rev {
|
|
holds = append(holds, fmt.Sprintf("the indexer has not yet taken in %.12s", heads[n-1].commit))
|
|
}
|
|
slices.Sort(holds)
|
|
r.Holds = slices.Compact(holds)
|
|
}
|
|
return r
|
|
}
|
|
|
|
// moduleReach is how far one module reached, the source revision it holds the reached head before (noBound when none), and
|
|
// what holds it, in words. A node counts for the fingerprint its running have runs, healthy or not (ADR 0556 clause 2).
|
|
func (m *Model) moduleReach(name string, mod delivery.Module, heads []headAt, wantKeys []string) (delivery.ModuleReach, uint64, string) {
|
|
mr := delivery.ModuleReach{FP: mod.FP2}
|
|
hist := m.modHist[name]
|
|
if n := len(hist); n == 0 || hist[n-1].FP2 != mod.FP2 || hist[n-1].Commit != mod.Commit {
|
|
hist = append(slices.Clone(hist), mod) // the value watched now is the newest, whatever the history watch has given
|
|
}
|
|
for i := len(hist) - 1; i >= 0 && len(mr.Changes) < delivery.MostChanges; i-- {
|
|
c := delivery.Change{Commit: hist[i].Commit, FP: hist[i].FP2, Src: hist[i].Src}
|
|
if c.Src == 0 {
|
|
c.Src = revOf(heads, c.Commit)
|
|
}
|
|
if n := len(mr.Changes); n > 0 && mr.Changes[n-1].FP == c.FP {
|
|
mr.Changes[n-1] = c // the same fingerprint again, back to back: its first appearance is the older commit
|
|
continue
|
|
}
|
|
mr.Changes = append(mr.Changes, c)
|
|
}
|
|
var waiting []string
|
|
floor, floorNode, unset := -1, "", ""
|
|
for _, key := range wantKeys {
|
|
node, _ := splitKey(key)
|
|
w := m.wants[key]
|
|
if w.err != "" || w.value.Removed || w.value.FP == "" {
|
|
continue
|
|
}
|
|
mr.Nodes++
|
|
h, ok := m.haves[key]
|
|
switch {
|
|
case !ok || h.err != "" || h.have.FP == "":
|
|
waiting = append(waiting, fmt.Sprintf("%s on %s runs nothing yet", name, node))
|
|
unset = waiting[len(waiting)-1]
|
|
continue
|
|
case !runs(h.have.Phase):
|
|
waiting = append(waiting, fmt.Sprintf("%s on %s is %s at %s, not running", name, node, h.have.Phase, short(h.have.FP)))
|
|
unset = waiting[len(waiting)-1]
|
|
continue
|
|
case h.have.FP != w.value.FP:
|
|
waiting = append(waiting, fmt.Sprintf("%s on %s runs %s, not yet the %s it wants", name, node, short(h.have.FP), short(w.value.FP)))
|
|
case h.have.Health == delivery.HealthFailed:
|
|
waiting = append(waiting, fmt.Sprintf("%s on %s runs %s and its health check failed", name, node, short(h.have.FP)))
|
|
case h.have.Health != delivery.HealthOK:
|
|
waiting = append(waiting, fmt.Sprintf("%s on %s runs %s, not yet healthy", name, node, short(h.have.FP)))
|
|
}
|
|
i := slices.IndexFunc(mr.Changes, func(c delivery.Change) bool { return c.FP == h.have.FP })
|
|
if i < 0 {
|
|
unset = fmt.Sprintf("%s on %s runs %s, which is not among its %d known changes", name, node, short(h.have.FP), len(mr.Changes))
|
|
waiting = append(waiting, unset)
|
|
continue
|
|
}
|
|
if i > floor {
|
|
floor, floorNode = i, node
|
|
}
|
|
}
|
|
mr.Waiting = strings.Join(waiting, "; ")
|
|
switch {
|
|
case mr.Nodes == 0:
|
|
return mr, noBound, ""
|
|
case unset != "":
|
|
// A node that wants the module runs nothing countable of it (no have, not running, or a build older than the kept
|
|
// history): the module holds no head of the repository back, which would make every apply anywhere, or one module
|
|
// stuck failed, freeze the whole repository (review of issue 1280, ADR 0556 clause 2). Its words name it, and a merge
|
|
// among its changes is not yet on the machines (notYet).
|
|
return mr, noBound, ""
|
|
}
|
|
mr.Floor = mr.Changes[floor].Commit
|
|
if floor == 0 {
|
|
return mr, noBound, ""
|
|
}
|
|
next := mr.Changes[floor-1]
|
|
words := fmt.Sprintf("%s on %s runs the build of %.12s, not yet that of %.12s", name, floorNode, mr.Floor, next.Commit)
|
|
if next.Src == 0 {
|
|
// The next change's order is not known: the head is held at the floor's own revision, or before every head when that is
|
|
// not known either.
|
|
if own := mr.Changes[floor].Src; own != 0 {
|
|
return mr, own + 1, words
|
|
}
|
|
return mr, 0, words
|
|
}
|
|
return mr, next.Src, words
|
|
}
|
|
|
|
// runs is whether a have in this phase runs the fingerprint it names: running, being checked after its apply, or held after a
|
|
// failed backup, which keeps running what it has (ADR 0539 clause 16). A have backing up or applying, failed or removed runs
|
|
// nothing the derivation can count.
|
|
func runs(p delivery.Phase) bool {
|
|
return p == delivery.PhaseRunning || p == delivery.PhaseChecking || p == delivery.PhaseHeld
|
|
}
|
|
|
|
// short is a fingerprint's first twelve characters, after its form.
|
|
func short(fp string) string {
|
|
fp = strings.TrimPrefix(fp, delivery.SourcePrefix2)
|
|
if len(fp) > 12 {
|
|
fp = fp[:12]
|
|
}
|
|
return fp
|
|
}
|
|
|
|
// reachReady is whether every watch the derivation reads handed over what stood at its start.
|
|
func (m *Model) reachReady() bool {
|
|
for _, b := range reachBuckets {
|
|
if !m.initial[b] {
|
|
return false
|
|
}
|
|
}
|
|
return m.source != nil && m.source.err == ""
|
|
}
|
|
|
|
// reachNow derives the reach of every repository the source names, and answers the values that changed and the reached
|
|
// heads not yet said. It is called with the lock held.
|
|
func (m *Model) reachNow() ([]ReachWrite, []DeliveredEvent) {
|
|
if (m.reachPut == nil && m.deliveredEvent == nil) || !m.reachReady() {
|
|
return nil, nil
|
|
}
|
|
var writes []ReachWrite
|
|
var events []DeliveredEvent
|
|
wants := m.wantKeys()
|
|
for _, repository := range slices.Sorted(maps.Keys(m.source.heads)) {
|
|
key, err := delivery.RepositoryKey(repository)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
r := m.deriveReach(repository, wants)
|
|
if prev, ok := m.reach[key]; !ok || !delivery.SameReach(prev, r) {
|
|
if data, err := delivery.EncodeReach(r); err == nil {
|
|
m.reach[key] = r
|
|
writes = append(writes, ReachWrite{Key: key, Value: data})
|
|
}
|
|
}
|
|
notYet := notYetOf(r)
|
|
said := strings.Join(notYet, ",")
|
|
if r.Reached != "" && (r.ReachedSrc > m.reachSaid[key] || said != m.reachNotYet[key]) {
|
|
m.reachSaid[key], m.reachNotYet[key] = r.ReachedSrc, said
|
|
events = append(events, DeliveredEvent{Revision: r.ReachedSrc, Heads: map[string]string{r.Repository: r.Reached},
|
|
Repository: r.Repository, NotYet: notYet})
|
|
}
|
|
}
|
|
return writes, events
|
|
}
|
|
|
|
// notYetOf are the merges at or before a repository's reached head that are not yet on the machines: the changes of each module
|
|
// a node wants with no floor (a node runs nothing countable of it), sorted. A consumer takes every other merge at or before
|
|
// the reached head as on the machines.
|
|
func notYetOf(r delivery.Reach) []string {
|
|
var out []string
|
|
for _, mr := range r.Modules {
|
|
if mr.Nodes == 0 || mr.Floor != "" {
|
|
continue
|
|
}
|
|
for _, c := range mr.Changes {
|
|
out = append(out, c.Commit)
|
|
}
|
|
}
|
|
slices.Sort(out)
|
|
return slices.Compact(out)
|
|
}
|
|
|
|
// Reach is the reach of every repository as the view derived it last, by key.
|
|
func (m *Model) Reach() map[string]delivery.Reach {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
return maps.Clone(m.reach)
|
|
}
|