Keep a layer's placement beside it, and say the layer whole (issue 651)

The settings verb's path splits a layer it is given into its config values and
its placement, so what a person types still works; a layer reads back and goes
into the history whole.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 00:42:37 +00:00
co-authored by Claude Opus 5.5
parent 7530c2aeac
commit 02b542a059
2 changed files with 99 additions and 26 deletions
+78 -19
View File
@@ -839,7 +839,18 @@ func (i *Inventory) KeepSettings(ctx context.Context, nodeName, module string, v
}
func (i *Inventory) setSettings(ctx context.Context, nodeName, module string, values map[string]any, judge bool, setBy string) error {
raw, err := json.Marshal(values)
// **Placement is not config** (novox/hq ADR 0348 clause 11, issue 651): the layer is said whole, as
// it always was, and its placement keys are kept beside the assignment, never in the settings row.
// Both are written in one transaction, and the layer a set replaces is the whole of both.
config, placement := catalogue.SplitPlacement(values)
if config == nil {
config = map[string]any{}
}
raw, err := json.Marshal(config)
if err != nil {
return err
}
placed, err := placementJSON(placement)
if err != nil {
return err
}
@@ -881,6 +892,9 @@ func (i *Inventory) setSettings(ctx context.Context, nodeName, module string, va
do update set values = excluded.values, set_at = now(), set_by = excluded.set_by`, module, raw, nullable(setBy)); err != nil {
return wrapModule(err, module)
}
if err := keepPlacement(ctx, tx, nil, module, placed, setBy); err != nil {
return err
}
return tx.Commit(ctx)
}
node, err := i.NodeByName(ctx, nodeName)
@@ -912,6 +926,9 @@ func (i *Inventory) setSettings(ctx context.Context, nodeName, module string, va
if err != nil {
return wrapModule(err, module)
}
if err := keepPlacement(ctx, tx, &node.ID, module, placed, setBy); err != nil {
return err
}
// A given port replaces what the mesh assigned for that port: the assignment is given back,
// so the number is free for the next module rather than held for ever in the name of a port
// that now lives elsewhere.
@@ -942,21 +959,15 @@ func (i *Inventory) judgeSettings(ctx context.Context, nodeName, module string,
return err
}
where, from, adopted = nodeName, nodeName, node.Adopted
var meshWide []byte
err = i.store.Pool().QueryRow(ctx,
`select values from settings where module = $1 and node is null`, module).Scan(&meshWide)
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
under, has, err := i.Layer(ctx, "", module)
if err != nil {
return err
}
if len(meshWide) > 0 {
var under map[string]any
if err := json.Unmarshal(meshWide, &under); err != nil {
return err
}
layers = append(layers, catalogue.Layer{From: catalogue.MeshWideLayer, Values: under})
if has {
layers = append(layers, catalogue.LayerOf(catalogue.MeshWideLayer, under))
}
}
layers = append(layers, catalogue.Layer{From: from, Values: values})
layers = append(layers, catalogue.LayerOf(from, values))
if err := catalogue.JudgeSettings(m, layers, adopted); err != nil {
return fmt.Errorf("refused: %s on %s cannot compose with the layer %q — %w", module, where, from, err)
}
@@ -1089,7 +1100,8 @@ func refuseGivenCollisions(ctx context.Context, tx pgx.Tx, nodeID any, node, mod
}
func wrapModule(err error, module string) error {
if err != nil && strings.Contains(err.Error(), "settings_module_fkey") {
if err != nil && (strings.Contains(err.Error(), "settings_module_fkey") ||
strings.Contains(err.Error(), "placement_module_fkey")) {
return fmt.Errorf("%w: %s", ErrNoSuchModule, module)
}
return err
@@ -1130,6 +1142,11 @@ func (i *Inventory) ClearSettingsBy(ctx context.Context, nodeName, module, clear
`delete from settings where module = $1 and node is not distinct from $2::uuid`, module, nodeID); err != nil {
return err
}
// And its placement, which is the same layer as a person says it (novox/hq ADR 0348 clause 11).
if _, err := tx.Exec(ctx,
`delete from placement where module = $1 and node is not distinct from $2::uuid`, module, nodeID); err != nil {
return err
}
return tx.Commit(ctx)
}
@@ -1142,10 +1159,13 @@ func (i *Inventory) SettingsFor(ctx context.Context, nodeName, module string) ([
if err != nil {
return nil, err
}
// Each layer's placement beside it, from the placement the controller holds (novox/hq ADR 0348
// clause 11): a layer's Values hold its config values only, its Placement where things are.
rows, err := i.store.Pool().Query(ctx,
`select node is null, values from settings
where module = $1 and (node is null or node = $2)
order by node is null desc`, module, node.ID)
`select s.node is null, s.values, p.values from settings s
left join placement p on p.module = s.module and p.node is not distinct from s.node
where s.module = $1 and (s.node is null or s.node = $2)
order by s.node is null desc`, module, node.ID)
if err != nil {
return nil, err
}
@@ -1154,23 +1174,62 @@ func (i *Inventory) SettingsFor(ctx context.Context, nodeName, module string) ([
var layers []catalogue.Layer
for rows.Next() {
var meshWide bool
var raw []byte
if err := rows.Scan(&meshWide, &raw); err != nil {
var raw, placed []byte
if err := rows.Scan(&meshWide, &raw, &placed); err != nil {
return nil, err
}
values := map[string]any{}
if err := json.Unmarshal(raw, &values); err != nil {
return nil, err
}
var placement map[string]any
if len(placed) > 0 {
if err := json.Unmarshal(placed, &placement); err != nil {
return nil, err
}
}
from := nodeName
if meshWide {
from = catalogue.MeshWideLayer
}
layers = append(layers, catalogue.Layer{From: from, Values: values})
layers = append(layers, catalogue.Layer{From: from, Values: values, Placement: placement})
}
return layers, rows.Err()
}
// placementJSON is a layer's placement as the placement table keeps it, or nil when it has none.
func placementJSON(placement map[string]any) ([]byte, error) {
if len(placement) == 0 {
return nil, nil
}
return json.Marshal(placement)
}
// keepPlacement writes one layer's placement beside its settings row, in the write's transaction: the
// placement the layer says, or none when it says none — a set replaces the layer whole, placement too.
func keepPlacement(ctx context.Context, tx pgx.Tx, nodeID *string, module string, placed []byte, setBy string) error {
if placed == nil {
_, err := tx.Exec(ctx,
`delete from placement where module = $1 and node is not distinct from $2::uuid`, module, nodeID)
return err
}
var err error
if nodeID == nil {
_, err = tx.Exec(ctx,
`insert into placement (node, module, values, set_by) values (null, $1, $2, $3)
on conflict (module) where node is null
do update set values = excluded.values, set_at = now(), set_by = excluded.set_by`,
module, placed, nullable(setBy))
} else {
_, err = tx.Exec(ctx,
`insert into placement (node, module, values, set_by) values ($1, $2, $3, $4)
on conflict (node, module) where node is not null
do update set values = excluded.values, set_at = now(), set_by = excluded.set_by`,
*nodeID, module, placed, nullable(setBy))
}
return wrapModule(err, module)
}
// PinProvision records which provider a machine gets a provision from: a module, and the node it
// runs on — both, always (novox/hq #258). A provision comes from a module, and the same module on
// two machines is two answers, so neither half alone says which.
+21 -7
View File
@@ -20,9 +20,11 @@ func (i *Inventory) Layer(ctx context.Context, nodeName, module string) (map[str
if err != nil {
return nil, false, err
}
// Said whole, as it was set: its config values and the placement kept beside them (novox/hq ADR 0348
// clause 11), so a person reading it before setting it sees every key they may keep or remove.
var raw []byte
err = i.store.Pool().QueryRow(ctx,
`select values from settings where module = $1 and node is not distinct from $2::uuid`,
err = i.store.Pool().QueryRow(ctx, `select `+layerWhole+` from settings s
where s.module = $1 and s.node is not distinct from $2::uuid`,
module, nodeID).Scan(&raw)
if errors.Is(err, pgx.ErrNoRows) {
return nil, false, nil
@@ -46,6 +48,9 @@ type PastLayer struct {
ReplacedBy string
// SetBy is who had set the layer, when it was kept (novox/hq ADR 0277); empty for one set before that was kept.
SetBy string
// Why is why it went, where that was said: "moved to placement, ADR 0348 clause 11" for a layer whose
// placement keys migration 0097 moved out of it (ReplacedBy "moved").
Why string
}
// LayerOrigin is who set a layer and when (novox/hq ADR 0277): empty words for a layer set before who set it was
@@ -79,7 +84,7 @@ func (i *Inventory) SettingsHistory(ctx context.Context, nodeName, module string
return nil, err
}
rows, err := i.store.Pool().Query(ctx,
`select values, set_at, replaced_at, replaced_by, set_by from settings_history
`select values, set_at, replaced_at, replaced_by, set_by, why from settings_history
where module = $1 and node is not distinct from $2::uuid order by replaced_at desc`,
module, nodeID)
if err != nil {
@@ -90,13 +95,16 @@ func (i *Inventory) SettingsHistory(ctx context.Context, nodeName, module string
for rows.Next() {
var raw []byte
var p PastLayer
var by *string
if err := rows.Scan(&raw, &p.SetAt, &p.ReplacedAt, &p.ReplacedBy, &by); err != nil {
var by, why *string
if err := rows.Scan(&raw, &p.SetAt, &p.ReplacedAt, &p.ReplacedBy, &by, &why); err != nil {
return nil, err
}
if by != nil {
p.SetBy = *by
}
if why != nil {
p.Why = *why
}
if err := json.Unmarshal(raw, &p.Values); err != nil {
return nil, err
}
@@ -107,9 +115,15 @@ func (i *Inventory) SettingsHistory(ctx context.Context, nodeName, module string
// keepReplacedSQL copies a layer into the history before it is replaced or cleared; run in the same
// transaction as the write where there is one, so a write that fails leaves no history of it.
//
// The layer is kept whole, as it was set: its config values and its placement (novox/hq ADR 0348 clause 11).
const keepReplacedSQL = `insert into settings_history (node, module, values, set_at, replaced_by, set_by)
select node, module, values, set_at, $3, set_by from settings
where module = $1 and node is not distinct from $2::uuid`
select s.node, s.module, ` + layerWhole + `, s.set_at, $3, s.set_by from settings s
where s.module = $1 and s.node is not distinct from $2::uuid`
// layerWhole is a settings row `s` with the placement kept beside it folded back in: the layer as it was set.
const layerWhole = `s.values || coalesce((select p.values from placement p
where p.module = s.module and p.node is not distinct from s.node), '{}'::jsonb)`
// layerNode is the node id of a layer, nil for the whole mesh's.
func (i *Inventory) layerNode(ctx context.Context, nodeName string) (*string, error) {