Files
mesh-controller/internal/inventory/chain_links.go
T
jschoubben 703badcf5a Name the commit, build and rollout on the assignment and the assignments on the rollout (issue 1095)
A send that records what a machine was sent writes, in the assignment of each module it carried a build of, the build, what it published, the commit, the rollout that sent it and since when: filled where the assignment names nothing, replaced when the commit moved, never blanked by a push by hand. A plan's sends are made as the plan, so they name it, and the plan lists the assignments each send changed with the build and commit before and after, the builds it carried and the steps taken. The builds, plans and node verbs say the links. node.sent_builds is written and read as before; SentBuilds from the assignments follows with the delivery that drops the column. Acceptance 10 is a test that enters by a merge heard and cut into a plan and writes no key by hand.
2026-10-18 03:22:57 +02:00

191 lines
6.8 KiB
Go

package inventory
import (
"context"
"errors"
"fmt"
"os"
"sort"
"time"
"github.com/jackc/pgx/v5"
"github.com/novox/mesh-controller/internal/chain"
)
// The links of the chain (novox/hq ADR 0408 §1, design 47 acceptance 10, issue 1095): a send that puts a build on a machine
// writes, in the assignment of the module there, the build, what it published, its commit and the rollout that sent it, and
// since when; the rollout lists the assignments it changed with the build before and after (the controller's send, in
// release_plan.go). One read of an assignment then names the commit, the build and the rollout that put it there, and one read
// of the rollout lists its assignments.
type rolloutKey struct{}
// RolloutModule is what a rollout sends of one module: the commit and the build of it.
type RolloutModule struct{ Commit, Build string }
type rolloutOf struct {
id string
modules map[string]RolloutModule
}
// WithRollout is a context for a send a rollout makes: what RecordSent writes in the assignments of the modules it carries
// names this rollout and the build it sent. A module the send carries beside these, that another walk is moving, is named
// by the build the mesh holds of its commit and by no rollout.
func WithRollout(ctx context.Context, id string, modules map[string]RolloutModule) context.Context {
return context.WithValue(ctx, rolloutKey{}, rolloutOf{id: id, modules: modules})
}
// AssignmentRuns is what each module assigned to a machine runs there, as its assignment names it: the build and the
// commit. Empty when the assignments are not on the bus, and for a module not sent yet.
func (i *Inventory) AssignmentRuns(ctx context.Context, node string) (map[string]chain.Ref, error) {
c, on, err := i.ChainReads(ctx, KindAssignments)
if err != nil || !on {
return nil, err
}
all, err := c.Assignments.All(ctx)
if err != nil {
return nil, err
}
out := map[string]chain.Ref{}
for _, k := range all {
a := k.Value
if a.Node == node && !a.Assigned.IsZero() && a.Stands() && (a.Build != "" || a.Commit != "") {
out[a.Module] = chain.Ref{Build: a.Build, Commit: a.Commit}
}
}
return out, nil
}
// AssignmentsOn are the assignments of a machine as the chain names them, by module: the standing ones, with their links.
func (i *Inventory) AssignmentsOn(ctx context.Context, node string) ([]chain.Assignment, error) {
c, on, err := i.ChainReads(ctx, KindAssignments)
if err != nil || !on {
return nil, err
}
all, err := c.Assignments.All(ctx)
if err != nil {
return nil, err
}
var out []chain.Assignment
for _, k := range all {
if a := k.Value; a.Node == node && !a.Assigned.IsZero() && a.Stands() {
out = append(out, a)
}
}
sort.Slice(out, func(a, b int) bool { return out[a].Module < out[b].Module })
return out, nil
}
// recordRuns writes what a send put on a machine into the assignments, once the send is recorded: for each module it carried
// a build of — by the commit — the build, what it published, the commit, the rollout and since when. **A module that runs the
// commit already keeps what it names**, filled where it names nothing and never blanked, so a send that carries it again, or a
// push by hand, changes nothing it knows. A module the machine is not assigned is no assignment to write.
func (i *Inventory) recordRuns(ctx context.Context, nodeID string, builds map[string]string, at time.Time) error {
c, on, err := i.ChainReads(ctx, KindAssignments)
if err != nil || !on || len(builds) == 0 {
return err
}
var node string
if err := i.store.Pool().QueryRow(ctx, `select name from node where id = $1`, nodeID).Scan(&node); errors.Is(err, pgx.ErrNoRows) {
return nil
} else if err != nil {
return err
}
rollout, _ := ctx.Value(rolloutKey{}).(rolloutOf)
modules := make([]string, 0, len(builds))
for m := range builds {
modules = append(modules, m)
}
sort.Strings(modules)
var failed []error
for _, m := range modules {
commit := builds[m]
if commit == "" {
continue
}
if err := i.recordRun(ctx, c, node, m, commit, rollout, at); err != nil {
failed = append(failed, fmt.Errorf("%s on %s: %w", m, node, err))
}
}
return errors.Join(failed...)
}
func (i *Inventory) recordRun(ctx context.Context, c *chain.Chain, node, module, commit string, rollout rolloutOf, at time.Time) error {
key := assignmentKey(node, module)
var build, rolloutID string
if sent, ours := rollout.modules[module]; ours && sent.Commit == commit {
build, rolloutID = sent.Build, rollout.id
}
var published *chain.Published
resolved := false
for try := 0; try < casTries; try++ {
cur, found, err := c.Assignments.Get(ctx, key)
if err != nil {
return err
}
if !found || cur.Value.Assigned.IsZero() || !cur.Value.Stands() {
return nil // nothing assigned there to say what it runs
}
same := cur.Value.Commit == commit
if same && cur.Value.Build != "" && (rolloutID == "" || cur.Value.Rollout != "") {
return nil // a send that carries what the machine runs already says nothing new
}
if !resolved {
// The build of the commit is looked up once, and only for a module that moved or names no build.
if build == "" {
if build, err = i.BuildOf(ctx, module, commit); err != nil {
return err
}
}
if build != "" {
if b, found, err := i.BuildByID(ctx, build); err != nil {
return err
} else if found {
published = publishedOf(b.Made)
}
}
resolved = true
}
next := cur.Value
if same {
// Already running it: fill what is missing, blank nothing.
if next.Build == "" {
next.Build = build
}
if next.Rollout == "" {
next.Rollout = rolloutID
}
if next.Since.IsZero() {
next.Since = at.UTC()
}
} else {
next.Commit, next.Build, next.Rollout, next.Since = commit, build, rolloutID, at.UTC()
next.Artifact, next.Package = "", nil
}
if published != nil && next.Artifact == "" && next.Package == nil && next.Build == build {
next.Artifact, next.Package = published.Artifact, published.Package
}
if next.Build == cur.Value.Build && next.Commit == cur.Value.Commit && next.Rollout == cur.Value.Rollout &&
next.Artifact == cur.Value.Artifact && (next.Package == nil) == (cur.Value.Package == nil) &&
next.Since.Equal(cur.Value.Since) {
return nil
}
if _, err := c.Assignments.Update(ctx, key, next, cur.Revision); errors.Is(err, chain.ErrMoved) {
continue
} else {
return err
}
}
return fmt.Errorf("the assignment kept moving under the write")
}
// saidRunsNot says a send's links could not be written, to the log of whoever sent: the send is away and is recorded, and
// a record of what it put where that could not be written is said, never a reason to call the send failed.
func saidRunsNot(node string, err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "mesh-controller: %s was sent its declaration, and the build, commit and rollout of each module "+
"could not be written in its assignments: %v\n", node, err)
}
}