A read naming no node (an empty node_haves) closed both windows for good (security review F3, second of a kind): the window embeds delivery.WindowHeld and steps by the shared rule, named being every node the read named, and keeps its own guard that a window once closed stays closed. A node-engine whose manifest-reads did not answer (a timeout included) counts as stating nothing as before, and the window's record now says so per node (Why), so registry_old_name tells a timeout from an old node-engine. A read that fails otherwise is an error the wake judges nothing on, no longer taken as not served. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
169 lines
6.8 KiB
Go
169 lines
6.8 KiB
Go
package holder
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
"slices"
|
|
"sort"
|
|
"time"
|
|
)
|
|
|
|
// The two windows distribution keeps (design 51): the old seat name `mesh-artifact-store`, served until every node whose
|
|
// declaration names an artifact applied a build whose node-engine states it reads the new names (ADR 0610 §4, ADR 0614
|
|
// §4), and the direct writes to the store, accepted from the mesh until every node's have of every build module runs a
|
|
// build that publishes through the seats. Each wakes at distribution's start, on each `delivered` and on the node-engine
|
|
// seat's `manifest-reads-changed` (ADR 0618 §4), never on a timer; each reads every node, away ones included, through
|
|
// node_haves and `manifest-reads`, never a watch of another module's state (ADR 0596).
|
|
const (
|
|
OldNameWindow = "old-name"
|
|
DirectWritesWindow = "direct-writes"
|
|
// WindowFindingAfter is how long a node holds a window open before it is distribution's finding naming the node.
|
|
WindowFindingAfter = 7 * 24 * time.Hour
|
|
// The events a window's close says on distribution's own family distribution.events (7 days, 16 MiB, drop
|
|
// oldest), never on a seat's `<seat>.events`.
|
|
OldNameClosedEvent = "old-name-closed"
|
|
DirectWritesClosedEvent = "direct-writes-closed"
|
|
// NewNamesCapability is the reader capability a node-engine states when it reads the new seat names (ADR 0614 §4).
|
|
NewNamesCapability = "seat:" + ImageSeat
|
|
)
|
|
|
|
// Window is one window's record, kept in distribution's state windows: what registry_old_name and
|
|
// registry_direct_writes answer. Its open, holding, since, closed and last holding are mesh-sdk's delivery.WindowHeld,
|
|
// the part every window keeper shares and steps by one rule (delivery.StepWindow).
|
|
type Window struct {
|
|
Name string `json:"name"`
|
|
delivery.WindowHeld
|
|
Dropped []DroppedNode `json:"dropped,omitempty"`
|
|
// Why says, of each node holding the window, why it holds it: what its read answered, a node-engine whose
|
|
// manifest-reads did not answer (an away node, an older node-engine, a timeout) included, so a timeout taken as
|
|
// stating nothing is told apart from a node-engine that states the old names.
|
|
Why map[string]string `json:"why,omitempty"`
|
|
}
|
|
|
|
// DroppedNode is one retired node dropped from a window by hand: a hand act.
|
|
type DroppedNode struct {
|
|
Node string `json:"node"`
|
|
By string `json:"by"`
|
|
Why string `json:"why"`
|
|
At time.Time `json:"at"`
|
|
}
|
|
|
|
// Closed is the body of a window's closing event.
|
|
type Closed struct {
|
|
Window string `json:"window"`
|
|
Seat string `json:"seat,omitempty"`
|
|
At time.Time `json:"at"`
|
|
LastHolding []string `json:"last_holding"`
|
|
Dropped []DroppedNode `json:"dropped,omitempty"`
|
|
}
|
|
|
|
// NodeRead is what distribution read of one node: the reader capabilities its node-engine states through
|
|
// `manifest-reads` (Serves false while it does not serve the verb: it states nothing; NotServed then says what the
|
|
// call answered), whether its declaration names an artifact, and its build modules' haves: whether each runs a build
|
|
// that publishes through the seats.
|
|
type NodeRead struct {
|
|
Node string `json:"node"`
|
|
Serves bool `json:"serves"`
|
|
NotServed string `json:"not_served,omitempty"`
|
|
Stated []string `json:"stated,omitempty"`
|
|
NamesArtifact bool `json:"names_artifact"`
|
|
BuildModules map[string]bool `json:"build_modules,omitempty"`
|
|
}
|
|
|
|
// WhyOldName says why a node holds the old name open.
|
|
func WhyOldName(n NodeRead) string {
|
|
if !n.Serves {
|
|
if n.NotServed != "" {
|
|
return "its node-engine's manifest-reads did not answer, so it counts as stating nothing: " + n.NotServed
|
|
}
|
|
return "its node-engine does not serve manifest-reads, so it states nothing"
|
|
}
|
|
return "its node-engine does not state " + NewNamesCapability
|
|
}
|
|
|
|
// WhyDirectWrites says why a node holds the direct writes open: its build modules running a build not heard
|
|
// publishing through the seats.
|
|
func WhyDirectWrites(n NodeRead) string {
|
|
var old []string
|
|
for m, publishes := range n.BuildModules {
|
|
if !publishes {
|
|
old = append(old, m)
|
|
}
|
|
}
|
|
sort.Strings(old)
|
|
return fmt.Sprintf("its %v run a build not heard publishing through the seats", old)
|
|
}
|
|
|
|
// HoldingOldName are the nodes holding the old name open: a node whose declaration names an artifact and whose
|
|
// node-engine does not state the new names (one not serving manifest-reads states nothing).
|
|
func HoldingOldName(nodes []NodeRead, dropped []DroppedNode) []string {
|
|
var out []string
|
|
for _, n := range nodes {
|
|
if isDropped(dropped, n.Node) || !n.NamesArtifact {
|
|
continue
|
|
}
|
|
if !n.Serves || !slices.Contains(n.Stated, NewNamesCapability) {
|
|
out = append(out, n.Node)
|
|
}
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
// HoldingDirectWrites are the nodes one of whose build modules' haves runs a build that does not yet publish through
|
|
// the seats.
|
|
func HoldingDirectWrites(nodes []NodeRead, dropped []DroppedNode) []string {
|
|
var out []string
|
|
for _, n := range nodes {
|
|
if isDropped(dropped, n.Node) {
|
|
continue
|
|
}
|
|
for _, publishes := range n.BuildModules {
|
|
if !publishes {
|
|
out = append(out, n.Node)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
func isDropped(dropped []DroppedNode, node string) bool {
|
|
return slices.ContainsFunc(dropped, func(d DroppedNode) bool { return d.Node == node })
|
|
}
|
|
|
|
// Step moves the window by mesh-sdk's one rule (delivery.StepWindow): named is every node the read named, dropped ones
|
|
// included, holding those holding the window at now. A read naming no node judges nothing. It answers the next record,
|
|
// the closing event when it closed in this step (said once), and the nodes holding it past WindowFindingAfter (the
|
|
// finding; cleared when it closes). A window once closed stays closed: a node that holds it later is no reason to serve
|
|
// the old name again or to take direct writes again.
|
|
func (w Window) Step(named, holding []string, now time.Time) (next Window, closed *Closed, overdue []string) {
|
|
next = w
|
|
if !w.Open && !w.Closed.IsZero() {
|
|
return next, nil, nil
|
|
}
|
|
held, shut, overdue := delivery.StepWindow(w.WindowHeld, named, holding, now, WindowFindingAfter)
|
|
next.WindowHeld = held
|
|
if !held.Open {
|
|
next.Why = nil
|
|
}
|
|
if !shut {
|
|
return next, nil, overdue
|
|
}
|
|
return next, &Closed{Window: w.Name, At: held.Closed, LastHolding: held.LastHolding, Dropped: w.Dropped}, nil
|
|
}
|
|
|
|
// Drop drops a retired node from the window by hand, a hand act with who and why.
|
|
func (w Window) Drop(node, by, why string, now time.Time) (Window, error) {
|
|
if node == "" || why == "" {
|
|
return w, fmt.Errorf("drop: a node and why are required (a hand act)")
|
|
}
|
|
if isDropped(w.Dropped, node) {
|
|
return w, fmt.Errorf("drop: %s is dropped from %s already", node, w.Name)
|
|
}
|
|
w.Dropped = append(w.Dropped, DroppedNode{Node: node, By: by, Why: why, At: now})
|
|
return w, nil
|
|
}
|