196 lines
6.4 KiB
Go
196 lines
6.4 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
)
|
|
|
|
// What relates two changes (novox/hq ADR 0304 decision 1, as ADR 0306 decision 3 reads it): two changes are
|
|
// related when they share a repository, a module, a delivery group's head, or an edge of the module graph
|
|
// between their modules. A module is every one the walk builds or sends: the modules its merges moved and the
|
|
// modules that depend on them, not only the ones the merges touched. Unrelated changes may walk beside each
|
|
// other (ADR 0304 decision 2, ADR 0282 decision 3); a closed batch is cut beside an open walk it is
|
|
// not related to, and a walk about to ask its first tier waits only for a started walk it is related to (issue 973,
|
|
// ADR 0407 decision 4).
|
|
|
|
// footprint is what one change reaches: its repositories (lower case), its modules, and its delivery group
|
|
// heads (the head branch names of its merges). unknown says a merge of it was kept without the modules it
|
|
// moves, so what it reaches is not known: it is related to every change, never wrongly to none.
|
|
type footprint struct {
|
|
repos, modules, heads map[string]bool
|
|
unknown string
|
|
}
|
|
|
|
func newFootprint() footprint {
|
|
return footprint{repos: map[string]bool{}, modules: map[string]bool{}, heads: map[string]bool{}}
|
|
}
|
|
|
|
// footprintOfWalk is a walk's footprint: the repositories it carries and the modules it plans. A walk keeps no
|
|
// head of its merges; kept are the merges the record holds for it, whose announcements say their heads, and
|
|
// may be nil.
|
|
func footprintOfWalk(p inventory.Plan, kept []inventory.BatchedMerge) footprint {
|
|
f := newFootprint()
|
|
for _, c := range p.Carried() {
|
|
f.repos[strings.ToLower(c.Repository)] = true
|
|
}
|
|
for name := range p.Modules {
|
|
f.modules[name] = true
|
|
}
|
|
for _, m := range kept {
|
|
f.repos[strings.ToLower(m.Repository)] = true
|
|
if h := announcedOf(m).Head; h != "" {
|
|
f.heads[h] = true
|
|
}
|
|
}
|
|
return f
|
|
}
|
|
|
|
// footprintOfBatch is an assembling batch's footprint: each merge's repository and head, and the modules its
|
|
// merges moved when heard, widened to their dependents (planOfMoves). A merge kept with no moves (one owed a
|
|
// later look, or kept for its packaging) is judged again at the cut and may move anything then: its batch is
|
|
// unknown, related to everything.
|
|
func footprintOfBatch(merges []inventory.BatchedMerge, edges []inventory.Edge) footprint {
|
|
f := newFootprint()
|
|
var moved []string
|
|
for _, m := range merges {
|
|
k := announcedOf(m)
|
|
f.repos[strings.ToLower(m.Repository)] = true
|
|
if k.Head != "" {
|
|
f.heads[k.Head] = true
|
|
}
|
|
if len(k.Moves) == 0 && f.unknown == "" {
|
|
f.unknown = "the modules a merge of " + m.Repository + " moves are not known until its batch is cut"
|
|
}
|
|
moved = append(moved, k.Moves...)
|
|
}
|
|
for _, name := range reachableFrom(moved, edges) {
|
|
f.modules[name] = true
|
|
}
|
|
return f
|
|
}
|
|
|
|
// related says whether two changes are related, and the first reason in plain words; false and empty when
|
|
// they are not.
|
|
func related(a, b footprint, edges []inventory.Edge) (bool, string) {
|
|
if a.unknown != "" {
|
|
return true, a.unknown
|
|
}
|
|
if b.unknown != "" {
|
|
return true, b.unknown
|
|
}
|
|
if r := firstShared(a.repos, b.repos); r != "" {
|
|
return true, "both change " + r
|
|
}
|
|
if m := firstShared(a.modules, b.modules); m != "" {
|
|
return true, "both move " + m
|
|
}
|
|
if h := firstShared(a.heads, b.heads); h != "" {
|
|
return true, "both are members of group " + h
|
|
}
|
|
for _, e := range edges {
|
|
if a.modules[e.From] && b.modules[e.To] || b.modules[e.From] && a.modules[e.To] {
|
|
return true, e.From + " depends on " + e.To + " (" + e.Kind + ")"
|
|
}
|
|
}
|
|
return false, ""
|
|
}
|
|
|
|
// firstShared is the first key, in order, both sets hold; empty when none.
|
|
func firstShared(a, b map[string]bool) string {
|
|
var both []string
|
|
for k := range a {
|
|
if b[k] {
|
|
both = append(both, k)
|
|
}
|
|
}
|
|
if len(both) == 0 {
|
|
return ""
|
|
}
|
|
sort.Strings(both)
|
|
return both[0]
|
|
}
|
|
|
|
// openWalk is a walk open beside a change: its id, what it reaches, and whether it is on the controller's own path.
|
|
type openWalk struct {
|
|
id string
|
|
fp footprint
|
|
own bool
|
|
}
|
|
|
|
// relatedOpen is the first open walk a change is related to, with the reason; empty when it is related to none
|
|
// and may walk beside them all (issue 973, ADR 0407 decision 4, ADR 0306 decision 3).
|
|
//
|
|
// **The controller's own path is related to everything** (decided for issue 973): a walk or batch that replaces
|
|
// the controller, the node-engine, the tool runner, the bus or mesh-delivery never runs beside another, and
|
|
// nothing runs beside it, whatever the footprints say.
|
|
func relatedOpen(cand footprint, candOwn bool, open []openWalk, edges []inventory.Edge) (behind, why string) {
|
|
for _, w := range open {
|
|
if candOwn {
|
|
return w.id, "it is on the controller's own path"
|
|
}
|
|
if w.own {
|
|
return w.id, w.id + " is on the controller's own path"
|
|
}
|
|
if ok, why := related(cand, w.fp, edges); ok {
|
|
return w.id, why
|
|
}
|
|
}
|
|
return "", ""
|
|
}
|
|
|
|
// ownWalk says a walk is on the controller's own path: its batch says so, or it plans a module of that path
|
|
// (issue 973).
|
|
func ownWalk(p inventory.Plan) bool {
|
|
if p.OwnPath() {
|
|
return true
|
|
}
|
|
for name := range p.Modules {
|
|
if _, own := onTheControllersPath[name]; own {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// startedWalks are the open walks started beside the walk id, made at created: a merge's walk past its wait, not
|
|
// the backlog's, with a tier asked, or on its way (let go, or waiting for nobody) before this one.
|
|
func startedWalks(plans []inventory.Plan, id string, created time.Time) []inventory.Plan {
|
|
var out []inventory.Plan
|
|
for _, q := range plans {
|
|
if q.ID == id || q.Release != nil || q.Waiting() {
|
|
continue
|
|
}
|
|
if q.Tier > 0 || askedAny(q) || q.Created.Before(created) {
|
|
out = append(out, q)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// openWalkOf is an open walk as a change is judged against it: what it reaches, from its planned modules and the
|
|
// merges the record holds for it.
|
|
func openWalkOf(ctx context.Context, inv *inventory.Inventory, p inventory.Plan) (openWalk, error) {
|
|
kept, err := inv.MergesOf(ctx, p.ID)
|
|
if err != nil {
|
|
return openWalk{}, err
|
|
}
|
|
return openWalk{id: p.ID, fp: footprintOfWalk(p, kept), own: ownWalk(p)}, nil
|
|
}
|
|
|
|
// openWalksOf are openWalkOf for each of plans.
|
|
func openWalksOf(ctx context.Context, inv *inventory.Inventory, plans []inventory.Plan) ([]openWalk, error) {
|
|
var out []openWalk
|
|
for _, p := range plans {
|
|
w, err := openWalkOf(ctx, inv, p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, w)
|
|
}
|
|
return out, nil
|
|
}
|