From b03bf4c118d95c9e48b4af39533e3b8389373902 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 28 Oct 2026 22:13:37 +0100 Subject: [PATCH] A node's answer holds as today what it holds today, so a seed that reproduces today is the same (issue 1126) --- go/compose/frombus/answer.go | 22 ++++++++++++++++++++++ go/compose/frombus/answer_rows_test.go | 7 +++++++ go/compose/frombus/frombus.go | 4 ++++ go/delivery/placement_candidate.go | 6 +++++- 4 files changed, 38 insertions(+), 1 deletion(-) diff --git a/go/compose/frombus/answer.go b/go/compose/frombus/answer.go index 1e05a20..031231f 100644 --- a/go/compose/frombus/answer.go +++ b/go/compose/frombus/answer.go @@ -99,6 +99,7 @@ func Answer(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]st for _, m := range sortedKeys(failed) { a.Refusals = append(a.Refusals, delivery.ModuleRefused{Module: m, Why: failed[m]}) } + heldAsToday(&a, s.HeldToday) a.Verdict = delivery.VerdictOf(a) return a } @@ -203,10 +204,31 @@ func Answer(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]st a.Refusals = append(a.Refusals, delivery.ModuleRefused{Module: m, Why: failed[m]}) } sort.Slice(a.Refusals, func(i, j int) bool { return a.Refusals[i].Module < a.Refusals[j].Module }) + heldAsToday(&a, s.HeldToday) a.Verdict = delivery.VerdictOf(a) return a } +// heldAsToday moves each refusal of a module the node holds today (Scope.HeldToday) into Held: the candidate holds it as +// the node does, which is no change (issue 1126: a seed reproduces today, and today holds it too). +func heldAsToday(a *delivery.CandidateAnswer, today []string) { + if len(today) == 0 { + return + } + kept := a.Refusals[:0] + for _, r := range a.Refusals { + if slices.Contains(today, r.Module) { + a.Held = append(a.Held, r) + } else { + kept = append(kept, r) + } + } + a.Refusals = kept + if len(a.Refusals) == 0 { + a.Refusals = nil + } +} + // maxCulprits bounds the modules one refusal is tried on, each a whole composition. const maxCulprits = 8 diff --git a/go/compose/frombus/answer_rows_test.go b/go/compose/frombus/answer_rows_test.go index 15da9b4..e62289e 100644 --- a/go/compose/frombus/answer_rows_test.go +++ b/go/compose/frombus/answer_rows_test.go @@ -91,6 +91,13 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) { if lemursOK := slices.ContainsFunc(a.Added, func(m delivery.ModuleAdded) bool { return m.Module == "lemurs" }); !lemursOK { t.Errorf("lemurs, which composes, is not answered beside the refusals: %+v", a) } + // Issue 1126: a module the node holds today that the candidate holds too is held as today, no refusal: with every + // refused module held today the verdict is no refusal, and each is named under Held. + today := Answer(ctx, b, Mesh{}, Scope{Node: "n1", Placements: given(b), HeldToday: []string{"api", "spiegel", "web"}}, + map[string]string{"lemurs": fp("e"), "web": fp("1"), "api": fp("2"), "spiegel": fp("3")}, nil, "p-h1", 3) + if today.Verdict == delivery.CandidateRefused || len(today.Refusals) != 0 || len(today.Held) != 3 || today.Held[0].Why == "" { + t.Errorf("held as today: %+v", today) + } // The fold: spiegel runs inside the node's own composition at a build of the old form. Its resources are carried as // they run, each after the one it runs after: the node's own is the one it runs, and the node composes itself. diff --git a/go/compose/frombus/frombus.go b/go/compose/frombus/frombus.go index 979502c..b287751 100644 --- a/go/compose/frombus/frombus.go +++ b/go/compose/frombus/frombus.go @@ -124,6 +124,10 @@ type Scope struct { Placements map[string]*delivery.Placement InOwn []string Running []delivery.Composed + // HeldToday is, in an answer, the modules the node holds today ("node" for its own composition): their have's phase + // is held, or they are held inside its own composition. A candidate that holds one too holds it as today, which is + // said under CandidateAnswer.Held and is no refusal (issue 1126). + HeldToday []string // SameValue, in an answer, says whether two sealed values open to the same value (the node opens them with its own // key), so a differing resource shows a value sealed again as such (delivery.CompareComposedWith). Nil shows them // as differing. diff --git a/go/delivery/placement_candidate.go b/go/delivery/placement_candidate.go index ad86720..241bbce 100644 --- a/go/delivery/placement_candidate.go +++ b/go/delivery/placement_candidate.go @@ -3,6 +3,7 @@ package delivery import ( "encoding/json" "fmt" + "slices" "strings" ) @@ -121,6 +122,9 @@ type CandidateAnswer struct { Added []ModuleAdded `json:"added,omitempty"` Removed []string `json:"removed,omitempty"` Refusals []ModuleRefused `json:"refusals,omitempty"` + // Held is each module the candidate cannot compose that the node holds today too, with why: held as today, which + // changes nothing and counts for no verdict (issue 1126: a seed reproduces today). + Held []ModuleRefused `json:"held,omitempty"` // Refused is why the node could not compose with the candidate at all; Refusals is then empty. Refused string `json:"refused,omitempty"` // Returns is, for a proposal that takes the node's placement key away, what the node does then: it returns to the @@ -233,7 +237,7 @@ func (a CandidateAnswer) check() error { return fmt.Errorf("delivery: a module the candidate adds is not named") } } - for _, d := range a.Refusals { + for _, d := range append(slices.Clone(a.Refusals), a.Held...) { if d.Module == "" || d.Why == "" { return fmt.Errorf("delivery: a candidate refusal names no module or no why") }