retire takes the definition's warrant (the hand act's id, naming node, module, name and ask) and answers retired; own-retired, renamed, retiring and retired carry exactly the definition's bodies on its subjects. The whole-module tests decode what the module published strictly into the generated types. The modules require mesh-contracts, to be pinned with the group (a local replace in an uncommitted go.work until then). Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
495 lines
16 KiB
Go
495 lines
16 KiB
Go
package main
|
|
|
|
// A config value renamed (novox/hq ADR 0609 §4, design 32 *A config value renamed*).
|
|
//
|
|
// A declaration naming `renamed-from` moves the value set under the old name to the new name, with the value's trust
|
|
// (its Value, whole), and opens a **rename window** per node: until that node's node-engine has applied a build of the
|
|
// module that reads the new name, the old name still answers with the new value (Resolve). A window closes on the
|
|
// status view's `delivered` event and, for a node that was away, on the node-engine seat's `manifest-reads-changed`
|
|
// (Windows). Its state is `renames`, key `<module>.<to>`; what it says is `renamed` on the stream `config-events`.
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// The state, the stream's events and the bounds this part keeps.
|
|
const (
|
|
RenamesState = "renames"
|
|
// EventRenamed is `renamed`, EventRetiring `retiring` and EventRetired `retired`, the family `config.events` of the
|
|
// seat's definition (design 26), under the holder's subjects: each carried by the stream StreamName, kept seven days.
|
|
EventRenamed = "renamed"
|
|
EventRetiring = "retiring"
|
|
EventRetired = "retired"
|
|
StreamName = "config-events"
|
|
// HeldFinding is how long a window or a retirement may stay open before it is a finding naming the node.
|
|
HeldFinding = 7 * 24 * time.Hour
|
|
)
|
|
|
|
// Rename is one rename, as `renames` keeps it, key `<module>.<to>`.
|
|
type Rename struct {
|
|
Module string `json:"module"`
|
|
From string `json:"from"`
|
|
To string `json:"to"`
|
|
At time.Time `json:"at"`
|
|
// Moved are the nodes a value was moved on, by the last move of each.
|
|
Moved []string `json:"moved,omitempty"`
|
|
// Nodes are the windows, by node.
|
|
Nodes map[string]Window `json:"nodes"`
|
|
// Said says `renamed` was emitted; false until the bus took it.
|
|
Said bool `json:"said"`
|
|
}
|
|
|
|
// Window is one node's rename window: open until Closed is set.
|
|
type Window struct {
|
|
Since time.Time `json:"since"`
|
|
Closed *time.Time `json:"closed,omitempty"`
|
|
// How is what closed it: "applied" (the node runs a build reading the new name), "unplaced" (the module left the
|
|
// node), or "by hand" (config_rename_close).
|
|
How string `json:"how,omitempty"`
|
|
}
|
|
|
|
// Open is the nodes whose window is open, sorted.
|
|
func (r Rename) Open() []string {
|
|
var out []string
|
|
for node, w := range r.Nodes {
|
|
if w.Closed == nil {
|
|
out = append(out, node)
|
|
}
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
// renameKey is a rename's key in `renames`.
|
|
func renameKey(module, to string) string { return module + "." + to }
|
|
|
|
// RenamedEvent is the body of `renamed`.
|
|
type RenamedEvent struct {
|
|
Module string `json:"module"`
|
|
From string `json:"from"`
|
|
To string `json:"to"`
|
|
At time.Time `json:"at"`
|
|
}
|
|
|
|
// readRenames is every rename kept, by key.
|
|
func (c *Config) readRenames() (map[string]Rename, error) {
|
|
out := map[string]Rename{}
|
|
err := c.each(RenamesState, func(key string, raw []byte) error {
|
|
var r Rename
|
|
if json.Unmarshal(raw, &r) != nil || r.Module == "" || renameKey(r.Module, r.To) != key {
|
|
return nil // not one this module wrote
|
|
}
|
|
if r.Nodes == nil {
|
|
r.Nodes = map[string]Window{}
|
|
}
|
|
out[key] = r
|
|
return nil
|
|
})
|
|
return out, err
|
|
}
|
|
|
|
// moduleValueKey is the key of a module's own value on a node.
|
|
func moduleValueKey(node, module, key string) string {
|
|
return Level{Kind: LevelModule, Node: node, Module: module}.ValueKey(key)
|
|
}
|
|
|
|
// applyRenames moves the values of every rename the trunk's manifests declare, opens the windows of the nodes the
|
|
// module is placed on, and writes `renamed` once: run on each resolution, under the lock, before the resolved values
|
|
// are written. s.Values and s.Renames are brought to what it did, so the same resolution sees it. A rename it refuses
|
|
// is a finding (findings.go), cleared once it moved or `renamed-from` left the manifest. It answers a line for what it
|
|
// did or could not do.
|
|
func (c *Config) applyRenames(s *Sources) []string {
|
|
held, err := c.readRenames()
|
|
if err != nil {
|
|
return []string{"the renames cannot be read: " + err.Error()}
|
|
}
|
|
now := c.now()
|
|
var said []string
|
|
seen := map[string]bool{}
|
|
for _, module := range modulesOf(s.Assignments) {
|
|
trunk, err := c.trunkDeclarations(module)
|
|
if err != nil {
|
|
continue // unresolved, said by the resolution
|
|
}
|
|
for _, to := range sortedKeys(trunk.Config) {
|
|
decl := trunk.Config[to]
|
|
if decl.RenamedFrom == "" {
|
|
continue
|
|
}
|
|
key := renameKey(module, to)
|
|
seen[key] = true
|
|
said = append(said, c.rename(s, held, trunk, to, decl, now)...)
|
|
}
|
|
}
|
|
// A rename whose `renamed-from` left the trunk's manifest goes once every window is closed.
|
|
for key, r := range held {
|
|
if seen[key] {
|
|
continue
|
|
}
|
|
if len(r.Open()) > 0 {
|
|
s.Renames[key] = r
|
|
continue
|
|
}
|
|
if err := c.Store.Delete(RenamesState, key); err != nil {
|
|
said = append(said, fmt.Sprintf("the rename %s is not removed: %v", key, err))
|
|
continue
|
|
}
|
|
delete(s.Renames, key)
|
|
c.clearFinding(FindingRenameRefused, r.Module, r.To)
|
|
c.clearFinding(FindingWindowHeld, r.Module, r.To)
|
|
}
|
|
c.sweepRenameFindings(s, now)
|
|
return said
|
|
}
|
|
|
|
// rename judges and applies one declared rename.
|
|
func (c *Config) rename(s *Sources, held map[string]Rename, trunk ModuleDeclarations, to string, decl Declaration,
|
|
now time.Time) []string {
|
|
module, from := trunk.Module, decl.RenamedFrom
|
|
key := renameKey(module, to)
|
|
nodes := nodesPlaced(s, module)
|
|
var said []string
|
|
refuse := func(why string) []string {
|
|
c.setFinding(renameRefused(module, to, from, why, now))
|
|
if r, ok := held[key]; ok {
|
|
s.Renames[key] = r
|
|
}
|
|
return []string{fmt.Sprintf("the rename of %s's %s to %s is refused: %s", module, from, to, why)}
|
|
}
|
|
if _, both := trunk.Config[from]; both {
|
|
return refuse("the manifest declares both names")
|
|
}
|
|
if from == to {
|
|
return refuse("the new name is the old one")
|
|
}
|
|
trusted := decl.Trusted
|
|
if p, ok := s.Predefined[decl.Predefined]; ok && p.Trusted {
|
|
trusted = true
|
|
}
|
|
type move struct {
|
|
node string
|
|
old, new Value
|
|
hadNew bool
|
|
oldKey string
|
|
newKey string
|
|
}
|
|
var moves []move
|
|
for _, node := range nodes {
|
|
oldKey, newKey := moduleValueKey(node, module, from), moduleValueKey(node, module, to)
|
|
ov, hadOld := s.Values[oldKey]
|
|
if !hadOld {
|
|
continue
|
|
}
|
|
nv, hadNew := s.Values[newKey]
|
|
switch {
|
|
case hadNew && string(compact(nv.Value)) != string(compact(ov.Value)):
|
|
return refuse(fmt.Sprintf("on %s both names hold a value, and they differ (%s under the old name, %s under the new)",
|
|
node, ov.Value, nv.Value))
|
|
case trusted && ov.Approved == "":
|
|
return refuse(fmt.Sprintf("on %s the value under the old name was set without the operator's Approve, and "+
|
|
"the new name is trusted (a rename keeps a value's trust, novox/hq ADR 0348 §8)", node))
|
|
}
|
|
moves = append(moves, move{node: node, old: ov, new: nv, hadNew: hadNew, oldKey: oldKey, newKey: newKey})
|
|
}
|
|
r, had := held[key]
|
|
if !had {
|
|
r = Rename{Module: module, From: from, To: to, At: now, Nodes: map[string]Window{}}
|
|
}
|
|
changed := !had
|
|
for _, m := range moves {
|
|
if !m.hadNew {
|
|
if err := c.Store.Put(ValuesState, m.newKey, m.old); err != nil {
|
|
return refuse("the value on " + m.node + " is not written under the new name: " + err.Error())
|
|
}
|
|
}
|
|
if err := c.Store.Delete(ValuesState, m.oldKey); err != nil {
|
|
return refuse("the old name's value on " + m.node + " is not removed: " + err.Error())
|
|
}
|
|
s.Values[m.newKey] = m.old
|
|
delete(s.Values, m.oldKey)
|
|
lvl := Level{Kind: LevelModule, Node: m.node, Module: module}
|
|
_ = c.Store.Append(HistoryLog, m.newKey, Change{Action: "rename", Level: &lvl, Key: to, Value: m.old.Value,
|
|
Was: m.old.Value, By: Module, Via: Module, Why: "renamed from " + from + " (renamed-from)", At: now})
|
|
if !slices.Contains(r.Moved, m.node) {
|
|
r.Moved = append(r.Moved, m.node)
|
|
sort.Strings(r.Moved)
|
|
}
|
|
changed = true
|
|
said = append(said, fmt.Sprintf("%s's %s on %s moved to %s", module, from, m.node, to))
|
|
}
|
|
for _, node := range nodes {
|
|
if _, ok := r.Nodes[node]; !ok {
|
|
r.Nodes[node] = Window{Since: now}
|
|
changed = true
|
|
}
|
|
}
|
|
c.clearFinding(FindingRenameRefused, module, to)
|
|
if !r.Said {
|
|
if err := c.Store.Emit(EventRenamed, RenamedEvent{Module: module, From: from, To: to, At: r.At}); err != nil {
|
|
said = append(said, fmt.Sprintf("renamed for %s's %s is not said yet: %v", module, to, err))
|
|
} else {
|
|
r.Said, changed = true, true
|
|
}
|
|
}
|
|
if changed {
|
|
if err := c.Store.Put(RenamesState, key, r); err != nil {
|
|
said = append(said, fmt.Sprintf("the rename %s is not kept: %v", key, err))
|
|
}
|
|
}
|
|
s.Renames[key] = r
|
|
return said
|
|
}
|
|
|
|
// nodesPlaced is the nodes a module is placed on, sorted.
|
|
func nodesPlaced(s *Sources, module string) []string {
|
|
var out []string
|
|
for _, a := range s.Assignments {
|
|
if a.Module == module {
|
|
out = append(out, a.Node)
|
|
}
|
|
}
|
|
sort.Strings(out)
|
|
return slices.Compact(out)
|
|
}
|
|
|
|
// resolveOldNames adds, for each rename with a window open on a node, the old name answering the new name's value on
|
|
// that node (design 32: until then mesh-config answers both names).
|
|
func resolveOldNames(s Sources, out map[string]Resolved) {
|
|
for _, key := range sortedKeys(s.Renames) {
|
|
r := s.Renames[key]
|
|
for _, node := range r.Open() {
|
|
nv, ok := out[ResolvedKey(node, r.Module, r.To)]
|
|
if !ok {
|
|
continue
|
|
}
|
|
old := nv
|
|
old.Key = r.From
|
|
out[ResolvedKey(node, r.Module, r.From)] = old
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- the windows --------------------------------------------------------------------------------------------
|
|
|
|
// Windows closes the rename windows (and the retirements waiting for nodes) that the delivery says are done, at start
|
|
// and on each `delivered`; for node, the one a returning node-engine's `manifest-reads-changed` names, it first asks
|
|
// the node-engine seat's `manifest-reads`: a node-engine that does not serve it states nothing and its window stays
|
|
// open (design 32). It answers a line for each window closed.
|
|
func (c *Config) Windows(node string) []string {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
said := c.closeWindows(node)
|
|
said = append(said, c.drainRetirements(node)...)
|
|
if len(said) > 0 {
|
|
if _, err := c.reconcileLocked(); err != nil {
|
|
said = append(said, "resolving after the windows: "+err.Error())
|
|
}
|
|
}
|
|
return said
|
|
}
|
|
|
|
// closeWindows is Windows' rename half; the caller holds the lock.
|
|
func (c *Config) closeWindows(only string) []string {
|
|
renames, err := c.readRenames()
|
|
if err != nil {
|
|
return []string{"the renames cannot be read: " + err.Error()}
|
|
}
|
|
if c.Delivery == nil {
|
|
return nil
|
|
}
|
|
s, err := c.sources()
|
|
if err != nil {
|
|
return []string{"the windows cannot be judged: " + err.Error()}
|
|
}
|
|
now := c.now()
|
|
var said []string
|
|
for _, key := range sortedKeys(renames) {
|
|
r := renames[key]
|
|
changed := false
|
|
for _, node := range r.Open() {
|
|
if only != "" && only != node {
|
|
continue
|
|
}
|
|
how, why := c.windowShut(s, r, node, only != "")
|
|
if why != "" {
|
|
said = append(said, fmt.Sprintf("%s's window for %s stays open on %s: %s", r.Module, r.To, node, why))
|
|
}
|
|
if how == "" {
|
|
continue
|
|
}
|
|
w := r.Nodes[node]
|
|
w.Closed, w.How = &now, how
|
|
r.Nodes[node] = w
|
|
changed = true
|
|
said = append(said, fmt.Sprintf("%s's window for %s is closed on %s (%s)", r.Module, r.To, node, how))
|
|
}
|
|
if !changed {
|
|
continue
|
|
}
|
|
if err := c.Store.Put(RenamesState, key, r); err != nil {
|
|
said = append(said, fmt.Sprintf("the rename %s is not kept: %v", key, err))
|
|
}
|
|
if len(r.Open()) == 0 {
|
|
c.clearFinding(FindingWindowHeld, r.Module, r.To)
|
|
}
|
|
}
|
|
return said
|
|
}
|
|
|
|
// windowShut says how a node's window may close now ("" when it may not) and, when it may not, why.
|
|
func (c *Config) windowShut(s Sources, r Rename, node string, returning bool) (how, why string) {
|
|
if !slices.Contains(nodesPlaced(&s, r.Module), node) {
|
|
return "unplaced", ""
|
|
}
|
|
if returning {
|
|
if err := c.Delivery.Reads(node); err != nil {
|
|
return "", "the node-engine states nothing yet (" + err.Error() + ")"
|
|
}
|
|
}
|
|
fp, found, err := c.Delivery.Have(node, r.Module)
|
|
switch {
|
|
case err != nil:
|
|
return "", "its have cannot be read: " + err.Error()
|
|
case !found || fp == "":
|
|
return "", "it reports no build of the module"
|
|
}
|
|
d, err := c.builtDeclarations(r.Module, fp)
|
|
if err != nil {
|
|
return "", "the build it runs cannot be read: " + err.Error()
|
|
}
|
|
if _, reads := d.Config[r.To]; !reads {
|
|
return "", "it still runs a build that reads " + r.From
|
|
}
|
|
return "applied", ""
|
|
}
|
|
|
|
// ---- the tools ---------------------------------------------------------------------------------------------
|
|
|
|
// RenameRow is one rename as the tools say it.
|
|
type RenameRow struct {
|
|
Module string `json:"module"`
|
|
From string `json:"from"`
|
|
To string `json:"to"`
|
|
At time.Time `json:"at"`
|
|
Moved []string `json:"moved,omitempty"`
|
|
// Open are the nodes holding the old name open; Closed the others, each with how.
|
|
Open []string `json:"open"`
|
|
Closed map[string]string `json:"closed,omitempty"`
|
|
}
|
|
|
|
// Renames lists the renames, the open ones only for the seat's verb, narrowed by module.
|
|
func (c *Config) Renames(module string, openOnly bool) ([]RenameRow, error) {
|
|
renames, err := c.readRenames()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rows := []RenameRow{}
|
|
for _, key := range sortedKeys(renames) {
|
|
r := renames[key]
|
|
if module != "" && r.Module != module {
|
|
continue
|
|
}
|
|
row := RenameRow{Module: r.Module, From: r.From, To: r.To, At: r.At, Moved: r.Moved, Open: r.Open()}
|
|
if row.Open == nil {
|
|
row.Open = []string{}
|
|
}
|
|
if openOnly && len(row.Open) == 0 {
|
|
continue
|
|
}
|
|
for node, w := range r.Nodes {
|
|
if w.Closed != nil {
|
|
if row.Closed == nil {
|
|
row.Closed = map[string]string{}
|
|
}
|
|
row.Closed[node] = w.How
|
|
}
|
|
}
|
|
rows = append(rows, row)
|
|
}
|
|
return rows, nil
|
|
}
|
|
|
|
// RenameAgain moves a rename's values now, the way a resolution does, and answers what it did: the recovery for a
|
|
// refused move once its cause is put right.
|
|
func (c *Config) RenameAgain(module, to string) (any, error) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if module == "" || to == "" {
|
|
return nil, fmt.Errorf("name the module and the new name of the rename")
|
|
}
|
|
r, err := c.reconcileLocked()
|
|
if err != nil {
|
|
return r, err
|
|
}
|
|
renames, err := c.readRenames()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, ok := renames[renameKey(module, to)]; !ok {
|
|
return nil, fmt.Errorf("no rename of %s to %s is kept: its trunk manifest declares no renamed-from for %s, or the rename is refused (config_renames and the findings say which)", module, to, to)
|
|
}
|
|
return map[string]any{"said": r.Renamed, "moved": r.Moved}, nil
|
|
}
|
|
|
|
// RenameClose drops a node from a rename's window by hand, recorded as a hand act.
|
|
func (c *Config) RenameClose(module, to, node, why string) (any, error) {
|
|
if strings.TrimSpace(why) == "" {
|
|
return nil, fmt.Errorf("say why the window is dropped: a hand act keeps its why")
|
|
}
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
key := renameKey(module, to)
|
|
renames, err := c.readRenames()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r, ok := renames[key]
|
|
if !ok {
|
|
return nil, fmt.Errorf("no rename of %s to %s is kept", module, to)
|
|
}
|
|
w, ok := r.Nodes[node]
|
|
if !ok || w.Closed != nil {
|
|
return nil, fmt.Errorf("%s holds no open window for %s's %s; it holds: %s", node, module, to, strings.Join(r.Open(), ", "))
|
|
}
|
|
now := c.now()
|
|
w.Closed, w.How = &now, "by hand"
|
|
r.Nodes[node] = w
|
|
if err := c.Store.HandAct(fmt.Sprintf("config_rename_close %s %s on %s", module, to, node), why,
|
|
"the operator dropped a node from the rename window of "+key); err != nil {
|
|
return nil, fmt.Errorf("the hand-act log does not hold it, so the window is not dropped: %w", err)
|
|
}
|
|
if err := c.Store.Put(RenamesState, key, r); err != nil {
|
|
return nil, fmt.Errorf("the window is not dropped: %w", err)
|
|
}
|
|
if len(r.Open()) == 0 {
|
|
c.clearFinding(FindingWindowHeld, r.Module, r.To)
|
|
}
|
|
if _, err := c.reconcileLocked(); err != nil {
|
|
return nil, err
|
|
}
|
|
return map[string]any{"closed": node, "open": r.Open()}, nil
|
|
}
|
|
|
|
// sweepRenameFindings writes a finding for each window held open past HeldFinding, naming the node, and clears one
|
|
// whose window closed.
|
|
func (c *Config) sweepRenameFindings(s *Sources, now time.Time) {
|
|
for _, key := range sortedKeys(s.Renames) {
|
|
r := s.Renames[key]
|
|
var held []string
|
|
for _, node := range r.Open() {
|
|
if now.Sub(r.Nodes[node].Since) >= HeldFinding {
|
|
held = append(held, node)
|
|
}
|
|
}
|
|
c.clearFinding(FindingWindowHeld, r.Module, r.To, held...)
|
|
for _, node := range held {
|
|
c.setFinding(windowHeld(r, node, now))
|
|
}
|
|
}
|
|
}
|