A want names the build alone from the second milestone, and the yes a person gave; a have waits for that yes with its judgement (issue 1126, ADR 0547 §2, §3)

This commit is contained in:
2026-10-26 18:39:46 +01:00
parent 9789c8379a
commit e49309d61c
4 changed files with 61 additions and 11 deletions
+17 -6
View File
@@ -166,8 +166,9 @@ func checkFingerprint(fp, of string) error {
return nil
}
// Want is a node's want for a module: the fingerprint FP the node should run and the digest Composed of the one
// composition of it the node may apply (ADR 0539 clause 1), or Removed for a module that left the node's modules (then
// Want is a node's want for a module: the fingerprint FP the node should run and, before ADR 0547's second milestone,
// the digest Composed of the one composition of it the node may apply (ADR 0539 clause 1; from that milestone the
// node composes its own, ADR 0547 §2, and a want names none), or Removed for a module that left the node's modules (then
// both are empty). When the module's newest fingerprint is not yet allowed on this node, Newest names it and Rule the
// rule that holds the node back; both are empty when FP is the newest. Backup is the answer to a failed backup before
// the apply, BackupAgain or BackupSkip (clause 16), empty when none was asked. Words say why in a sentence. The
@@ -185,6 +186,10 @@ type Want struct {
// is released. A put's want names no newest fingerprint or rule.
Put bool `json:"put,omitempty"`
Hold bool `json:"hold,omitempty"`
// Yes is the digest of a composition a person allowed the node to move to (novox/hq ADR 0547 §3): a move of a module
// of policy record that the node agent judged to recreate a container or move its data is applied only when its
// want names the composition so allowed. Empty when none was asked.
Yes string `json:"yes,omitempty"`
}
func (w Want) check() error {
@@ -203,11 +208,17 @@ func (w Want) check() error {
if err := checkFingerprint(w.FP, "a want's fingerprint"); err != nil {
return err
}
if w.Composed == "" {
return fmt.Errorf("delivery: the want of %s names no composition: a want names the digest of the one composition it allows (ADR 0539 clause 1)", w.FP)
// From ADR 0547's second milestone the node composes its own modules (§2), and a want names the build alone: no
// composition digest. A want written before names one, which is read as it was written.
if w.Composed != "" {
if err := checkDigest(w.Composed, "a want's composition digest"); err != nil {
return err
}
}
if err := checkDigest(w.Composed, "a want's composition digest"); err != nil {
return err
if w.Yes != "" {
if err := checkDigest(w.Yes, "the digest a want's yes allows"); err != nil {
return err
}
}
switch w.Backup {
case "", BackupAgain, BackupSkip:
+19 -1
View File
@@ -97,6 +97,10 @@ const (
// composition is applied, and the node waits for the answer in its want (novox/hq ADR 0539 clause 16). Detail
// carries the backup's own words.
PhaseHeld Phase = "held"
// PhaseWaitingYes is a module of policy record whose composed move the node agent judged to recreate a container or
// move its data (novox/hq ADR 0547 §3): nothing of it is applied, Judgement says the judgement and Waits the digest
// of the composition waiting, and the assigner asks a person; the node applies it once its want's Yes names Waits.
PhaseWaitingYes Phase = "waiting-yes"
)
// Health is what the module's own check says.
@@ -340,6 +344,10 @@ type Have struct {
// UserList is what the node-engine of the node holding mesh-broker wrote of the bus's user list, in the have of the
// bus module (novox/hq ADR 0547 §7, §8). Nil on every other have.
UserList *UserListWritten `json:"user-list,omitempty"`
// Judgement and Waits are, in phase waiting-yes, the node agent's judgement of the move and the digest of the
// composition that waits for a person's yes (ADR 0547 §3); empty otherwise.
Judgement Judgement `json:"judgement,omitempty"`
Waits string `json:"waits,omitempty"`
}
// UserListWritten is the bus's user list as the node-engine of the bus node wrote it to the bus module's file, applied
@@ -435,8 +443,18 @@ func decode(data []byte, into any) error {
func (h Have) check() error {
switch h.Phase {
case PhaseBackingUp, PhaseApplying, PhaseChecking, PhaseRunning, PhaseFailed, PhaseRemoved, PhaseHeld:
if h.Judgement != "" || h.Waits != "" {
return fmt.Errorf("delivery: only a have waiting for a yes names a judgement and the composition waiting")
}
case PhaseWaitingYes:
if h.Judgement != JudgedRecreates && h.Judgement != JudgedMovesData && h.Judgement != JudgedCannot {
return fmt.Errorf("delivery: a have waiting for a yes names the judgement %q, not recreates, moves-data or cannot-judge", h.Judgement)
}
if err := checkDigest(h.Waits, "the composition a have waits a yes for"); err != nil {
return err
}
default:
return fmt.Errorf("delivery: phase %q is not backing-up, applying, checking, running, failed, removed or held", h.Phase)
return fmt.Errorf("delivery: phase %q is not backing-up, applying, checking, running, failed, removed, held or waiting-yes", h.Phase)
}
if h.Composed != "" {
if err := checkDigest(h.Composed, "the have's composition digest"); err != nil {
+17
View File
@@ -367,6 +367,23 @@ func testPhaseHealthRefused(t *testing.T) {
if _, err := DecodeHave([]byte(`{"v":1,"phase":"running","health":"fine"}`)); err == nil {
t.Error("bad health decoded")
}
// ADR 0547 §3 (issue 1126): a have waits for a yes with its judgement and the composition waiting, and no other have
// names either.
waits := Have{Phase: PhaseWaitingYes, Health: HealthOK, Judgement: JudgedRecreates, Waits: "sha256:" + strings.Repeat("a", 64)}
if raw, err := EncodeHave(waits); err != nil {
t.Errorf("a have waiting for a yes: %v", err)
} else if back, err := DecodeHave(raw); err != nil || back.Judgement != JudgedRecreates || back.Waits != waits.Waits {
t.Errorf("a have waiting for a yes came back %+v, %v", back, err)
}
for name, h := range map[string]Have{
"neither judged": {Phase: PhaseWaitingYes, Health: HealthOK, Judgement: JudgedNeither, Waits: waits.Waits},
"no waiting digest": {Phase: PhaseWaitingYes, Health: HealthOK, Judgement: JudgedMovesData},
"running judged": {Phase: PhaseRunning, Health: HealthOK, Judgement: JudgedRecreates},
} {
if _, err := EncodeHave(h); err == nil {
t.Errorf("%s: encoded", name)
}
}
}
// 1131/AC7: the pulse's time to live is 120 seconds minus the move's age; at the bound the move is stalled.
+8 -4
View File
@@ -8,7 +8,8 @@ import (
func hashOf(c byte) string { return "sha256:" + digestOf(c) }
// 1943/AC3: a want naming its fingerprint and the digest of the composition it allows round-trips, and so does one
// carrying the answer to a failed backup; a want naming a fingerprint without a digest, a digest not of the sha256
// carrying the answer to a failed backup, and from ADR 0547's second milestone one naming the build alone or the yes
// a person gave; a digest not of the sha256
// form, a removed want naming a digest, an unknown backup answer and a rule past the seventh are refused.
func TestAWantNamesItsComposition(t *testing.T) {
for _, w := range []Want{
@@ -17,6 +18,9 @@ func TestAWantNamesItsComposition(t *testing.T) {
{FP: fp2of('a'), Composed: hashOf('1'), Backup: BackupSkip, Words: "the operator: go on without a backup"},
{FP: fp2of('a'), Composed: hashOf('1'), Newest: fp2of('b'), Rule: RuleComposed, Words: "no composition of b for ace yet"},
{Removed: true},
// ADR 0547 §2, §3 (issue 1126): from the second milestone a want names the build alone, and the yes a person gave.
{FP: fp2of('a')},
{FP: fp2of('a'), Yes: hashOf('3')},
} {
raw, err := EncodeWant(w)
if err != nil {
@@ -32,7 +36,7 @@ func TestAWantNamesItsComposition(t *testing.T) {
w Want
why string
}{
{Want{FP: fp2of('a')}, "names no composition"},
{Want{FP: fp2of('a'), Yes: "sha256:12"}, "a want's yes allows"},
{Want{FP: fp2of('a'), Composed: "sha256:12"}, "not sha256"},
{Want{Removed: true, Composed: hashOf('1')}, "removed want names no"},
{Want{FP: fp2of('a'), Composed: hashOf('1'), Backup: "later"}, `backup answer "later"`},
@@ -44,8 +48,8 @@ func TestAWantNamesItsComposition(t *testing.T) {
t.Errorf("EncodeWant(%+v) = %v, want a refusal saying %q", r.w, err, r.why)
}
}
if _, err := DecodeWant([]byte(`{"v":1,"fp":"` + fp2of('a') + `"}`)); err == nil || !strings.Contains(err.Error(), "names no composition") {
t.Errorf("a want of release 5, naming no composition, was read: %v", err)
if w, err := DecodeWant([]byte(`{"v":1,"fp":"` + fp2of('a') + `"}`)); err != nil || w.Composed != "" {
t.Errorf("a want naming the build alone (ADR 0547 §2) is not read: %+v, %v", w, err)
}
if RuleComposed != 7 {
t.Errorf("RuleComposed = %d, want the seventh rule", RuleComposed)