From e49309d61c0d27c345e2ccd84f9f2290ca96bae4 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 26 Oct 2026 18:39:46 +0100 Subject: [PATCH] =?UTF-8?q?A=20want=20names=20the=20build=20alone=20from?= =?UTF-8?q?=20the=20second=20milestone,=20and=20the=20yes=20a=20person=20g?= =?UTF-8?q?ave;=20a=20have=20waits=20for=20that=20yes=20with=20its=20judge?= =?UTF-8?q?ment=20(issue=201126,=20ADR=200547=20=C2=A72,=20=C2=A73)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go/delivery/assigner.go | 23 +++++++++++++++++------ go/delivery/delivery.go | 20 +++++++++++++++++++- go/delivery/delivery_test.go | 17 +++++++++++++++++ go/delivery/want_composed_test.go | 12 ++++++++---- 4 files changed, 61 insertions(+), 11 deletions(-) diff --git a/go/delivery/assigner.go b/go/delivery/assigner.go index 4aff2fd..7bb559e 100644 --- a/go/delivery/assigner.go +++ b/go/delivery/assigner.go @@ -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: diff --git a/go/delivery/delivery.go b/go/delivery/delivery.go index 1c7834b..1966d1b 100644 --- a/go/delivery/delivery.go +++ b/go/delivery/delivery.go @@ -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 { diff --git a/go/delivery/delivery_test.go b/go/delivery/delivery_test.go index 9e6b68c..2f3c480 100644 --- a/go/delivery/delivery_test.go +++ b/go/delivery/delivery_test.go @@ -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. diff --git a/go/delivery/want_composed_test.go b/go/delivery/want_composed_test.go index 12512e8..7c9d68d 100644 --- a/go/delivery/want_composed_test.go +++ b/go/delivery/want_composed_test.go @@ -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)