Files
mesh-catalog/modules/mesh-config/cmd/mesh-config/renames.go
T
jschoubben 553cf50da7 CAT-V4, CAT-C10: the vault's retire and the config events and renames verb match the seat definitions of mesh-contracts
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
2026-11-04 11:59:24 +01:00

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))
}
}
}