Files
mesh-controller/internal/chain/records.go
T
jschoubben fdab58ae4f Read what a node was sent from its assignment keys; a pulled-in module keeps a standing key; a hand send is a time (issue 1109)
SentBuilds answers from the keys of the node's assignments that name a commit, known when the node was sent anything and no key says its commit unknown. A send that names its builds writes the commit, build and rollout on each carried module's key (a module nothing assigns gets a standing key marked pulled-in, never a state) and clears what every other key of the node named; a send by hand writes sent-unknown, a time, on every key and clears their commits. A failed write of these fails the record of the send. node.sent_builds is no longer written. The plan pass ends a pulled-in key only when no send carries it.
2026-10-18 05:22:28 +02:00

267 lines
12 KiB
Go

// Package chain keeps the controller's chain on the bus alone (novox/hq ADR 0408, design 47): module, commit, build,
// published artifact or package, rollout and assignment are one chain, a bucket each of the bus's key-value store,
// each the only source of its kind, written by the controller that holds the lease by compare-and-set.
//
// **No key is removed.** A Backend has no delete and no purge (ADR 0408 §4): an assignment of a module taken off its
// node is kept, saying so. **A value is never cut** (§6): one over the largest the bus holds is refused, and the key
// stays as it was. **Each key keeps its earlier values** (§5), read back with History.
package chain
import (
"encoding/json"
"time"
)
// Ref names a build and the commit it was made from: one end of a change.
type Ref struct {
Build string `json:"build,omitempty"`
Commit string `json:"commit,omitempty"`
}
// Package is a release a build published to the package registry.
type Package struct {
Name string `json:"name"`
Version string `json:"version"`
}
// Published is what a build published: an artifact by its digest, or a package release by name and version.
type Published struct {
Artifact string `json:"artifact,omitempty"`
Package *Package `json:"package,omitempty"`
}
// Artifact is one thing a build made.
type Artifact struct {
Name string `json:"name"`
Kind string `json:"kind"`
Reference string `json:"reference"`
}
// ReadRepository is a repository a build read source from besides the module's own.
type ReadRepository struct {
Repository string `json:"repository"`
Ref string `json:"ref,omitempty"`
Paths []string `json:"paths,omitempty"`
Own bool `json:"own,omitempty"`
}
// Source is the build source a build read in one repository.
type Source struct {
Repository string `json:"repository,omitempty"`
Ref string `json:"ref,omitempty"`
Paths []string `json:"paths"`
}
// The states of a build record. A record in state asked is a request that no outcome has answered (issue 1091); one
// without a state is an outcome, and keeps the Request it was asked with.
const BuildAsked = "asked"
// The states of a request's Reask.
const (
ReaskPending = "pending"
ReaskClaimed = "claimed"
)
// Request is a build as it was asked for (novox/hq issue 325, ADR 0408 §1): kept in the build's own record under its id,
// first alone as state asked and then beside the outcome that answered it.
type Request struct {
// Repository and Seat are the source as the mesh spells it; Path is the module's directory in it, Ref the branch or
// commit asked, and Commit the commit that made the ask, where one did.
Repository string `json:"repository,omitempty"`
Seat string `json:"seat,omitempty"`
Path string `json:"path,omitempty"`
Ref string `json:"ref,omitempty"`
Commit string `json:"commit,omitempty"`
// For says who asked: merge, plan, build (a person), assign.
For string `json:"for,omitempty"`
// NotAsked is why the ask could not be handed over; OutcomeUnknown why an asker that handed it over stopped waiting.
NotAsked string `json:"not-asked,omitempty"`
OutcomeUnknown string `json:"outcome-unknown,omitempty"`
At time.Time `json:"at,omitzero"`
AtTerminal bool `json:"at-terminal,omitempty"`
// Reask is, on a request the move found never handed over, "pending" until the controller asks it again, "claimed"
// while it does, and the build id it was asked as once it has: published once, never twice (issue 981 note 7 Q5).
Reask string `json:"reask,omitempty"`
}
// Build is one attempt, whichever way it went, kept under its id. It names its module, its repository and commit, its
// reuse key (novox/hq ADR 0350) and what it published (ADR 0408 §1).
type Build struct {
ID string `json:"id"`
State string `json:"state,omitempty"`
Repository string `json:"repository"`
Ref string `json:"ref,omitempty"`
Module string `json:"module,omitempty"`
Commit string `json:"commit,omitempty"`
On string `json:"on,omitempty"`
Path string `json:"path,omitempty"`
Manifest json.RawMessage `json:"manifest,omitempty"`
Against []string `json:"against,omitempty"`
Mirrored []string `json:"mirrored,omitempty"`
Read []ReadRepository `json:"read,omitempty"`
Sources []Source `json:"sources,omitempty"`
SourceFingerprint string `json:"source-fingerprint,omitempty"`
Failed string `json:"failed,omitempty"`
Made []Artifact `json:"made,omitempty"`
Asked time.Time `json:"asked,omitzero"`
At time.Time `json:"at,omitzero"`
ReuseKey string `json:"reuse-key,omitempty"`
Published *Published `json:"published,omitempty"`
Request *Request `json:"request,omitempty"`
}
// Commit is one repository's commit a rollout carries.
type Commit struct {
Repository string `json:"repository"`
Branch string `json:"branch,omitempty"`
Commit string `json:"commit"`
Merged time.Time `json:"merged,omitzero"`
}
// Change is one assignment a rollout changed: the build and commit it ran before and the ones it runs after.
type Change struct {
Node string `json:"node"`
Module string `json:"module"`
Before Ref `json:"before"`
After Ref `json:"after"`
}
// Step is one tier sent to one machine.
type Step struct {
Tier int `json:"tier"`
Node string `json:"node"`
Sent *time.Time `json:"sent,omitempty"`
Reported *time.Time `json:"reported,omitempty"`
}
// Links are the fields a rollout gains on the chain: the builds it carried, the machine steps it has taken and the
// assignments it changed, each with the build before and after. The controller's plan embeds them, so the plan's own
// fields and these are one value (ADR 0408 §1).
type Links struct {
Builds []string `json:"builds,omitempty"`
Steps []Step `json:"steps,omitempty"`
Changed []Change `json:"changed,omitempty"`
}
// HeldMerge is a merge a rollout holds: the columns the controller's table of batched merges had, kept in the record of the
// batch or walk that answers it (novox/hq ADR 0276, issue 1092). A merge no plan answers (Waiting) stays in the record
// that held it last. Placed is when it entered this record, so one found in two records after a crash between the two
// writes is read from the one it entered last.
type HeldMerge struct {
Repository string `json:"repository"`
Branch string `json:"branch,omitempty"`
Commit string `json:"commit"`
Merged time.Time `json:"merged,omitzero"`
Heard time.Time `json:"heard,omitzero"`
Event json.RawMessage `json:"event,omitempty"`
Alone bool `json:"alone,omitempty"`
Waiting bool `json:"waiting,omitempty"`
Placed time.Time `json:"placed,omitzero"`
}
// Rollout is the controller's record of one batch's delivery, kept under the batch's id: the commit it fixed per
// repository, its tiers, the builds it carried and the assignments it changed (ADR 0408 §1). The controller's plan
// carries more (its modules, its times, its backups) beside these, under the same key.
type Rollout struct {
ID string `json:"id"`
State string `json:"state,omitempty"`
Commits []Commit `json:"commits,omitempty"`
Tiers [][]string `json:"tiers,omitempty"`
Links
}
// Assignment is one module on one node, kept under `<node>.<module>`. The first five fields are the ones its readers
// have always read, with the names they have always had (novox/hq ADR 0408 §3); the rest name the chain: the build, the
// artifact (by digest) or the package it runs, its commit, the rollout that put it there and since when.
type Assignment struct {
Node string `json:"node"`
Module string `json:"module"`
Assigned time.Time `json:"assigned,omitzero"`
RequiredBy string `json:"required-by,omitempty"`
Provisioning json.RawMessage `json:"provisioning,omitempty"`
Build string `json:"build,omitempty"`
Artifact string `json:"artifact,omitempty"`
Package *Package `json:"package,omitempty"`
Commit string `json:"commit,omitempty"`
Rollout string `json:"rollout,omitempty"`
Since time.Time `json:"since,omitzero"`
// Ended is when the assignment ended (an unassignment): absent while it stands. **No key of the chain is removed**
// (ADR 0408 §4), so an unassigned module keeps its key, saying so; every reader treats a value with Ended, or with a
// State, as no assignment. State is StatePending while the key holds an assignment waiting for its build, or one that
// was never made (issue 1094).
Ended time.Time `json:"ended,omitzero"`
State string `json:"state,omitempty"`
// Pending is the assignment's wait for its build (novox/hq ADR 0261), kept in the value of the key it waits for and, when
// it is made, carried into the value that stands: the record of what became of the wait.
Pending *Pending `json:"pending,omitempty"`
// PulledIn marks a standing value of a module that is on the machine only because another requires it, or because a
// send carried it: no time assigned, but a key all the same, so what the machine was sent of it is kept and read
// (issue 1109). Never a state: every reader treats a non-empty state as no assignment, and would take it off the machine.
PulledIn bool `json:"pulled-in,omitempty"`
// SentUnknown is when a send by hand made what the machine runs unknown (a declaration sent with no builds known): the
// commit is cleared with it. Absent normally; a send that names its builds clears it.
SentUnknown time.Time `json:"sent-unknown,omitzero"`
}
// StatePending is the State of a value that holds no assignment but a pending one.
const StatePending = "pending"
// The states of a pending assignment (novox/hq ADR 0261).
const (
PendingWaiting = "waiting"
PendingApplying = "applying"
PendingApplied = "applied"
PendingRefused = "refused"
PendingExpired = "expired"
PendingWithdrawn = "withdrawn"
)
// Pending is an assignment kept until its module is registered: the build it waits for, where that build is from, and the
// times of its claim and its end. Its id is p-<unix nanoseconds of Since>, unique for a key, which holds one at a time.
type Pending struct {
ID string `json:"id"`
State string `json:"state"`
Build string `json:"build,omitempty"`
Repository string `json:"repository,omitempty"`
Path string `json:"path,omitempty"`
Since time.Time `json:"since"`
Note string `json:"note,omitempty"`
Settled *time.Time `json:"settled,omitempty"`
Raised *time.Time `json:"raised,omitempty"`
Acknowledged *time.Time `json:"acknowledged,omitempty"`
Cleared *time.Time `json:"cleared,omitempty"`
// Earlier are the waits of this key that ended unmade and were replaced by this one before their condition was answered
// (taken back or cleared): each stays, with its own marks, so a replaced wait never leaves the operator without the
// condition it raised or was to raise. Dropped once answered.
Earlier []Pending `json:"earlier,omitempty"`
}
// Open says the assignment is still to be made.
func (p Pending) Open() bool { return p.State == PendingWaiting || p.State == PendingApplying }
// CanSettle says a pending assignment in state from may end in state to: a wait expires or is withdrawn, a claim becomes
// applied or refused, and nothing else.
func CanSettle(from, to string) bool {
switch from {
case PendingWaiting:
return to == PendingExpired || to == PendingWithdrawn
case PendingApplying:
return to == PendingApplied || to == PendingRefused
}
return false
}
// CanMove says a pending assignment in state from may go to state to without ending: claimed, or released again.
func CanMove(from, to string) bool {
return (from == PendingWaiting && to == PendingApplying) || (from == PendingApplying && to == PendingWaiting)
}
// Stands says the value is an assignment that holds: it has not ended and is not waiting in a state of its own.
func (a Assignment) Stands() bool { return a.Ended.IsZero() && a.State == "" }
// Key is an assignment's key.
func (a Assignment) Key() string { return a.Node + "." + a.Module }