Hold only the module whose manifest the mesh refuses, so one bad item no longer fails every node's composition (issue 774)
On 2026-10-30 at 19:13 power's bar item (mesh-catalog ca8d6f33) was refused by ParseManifest, and frombus's input returned that refusal for the whole read: every node-engine marked all its modules failed and no update could land. The third of a kind that day, after the bus user list and the provisioner's plans (PlansHeld). Quick fix: skip a manifest that does not parse in input's catalogue loop. Thorough fix: input follows the rule a build that cannot be read already follows there (refuse + stub), so Compose, Answer and PlansHeld all hold the one module: held with why on a node it is placed on, read for its offer alone (its unjudged stub, without items) elsewhere. A manifest the bus does not answer for still ends the read (transient, waited out by the caller), and Input (refuse unset, the check's whole composition) still refuses. Chose thorough: one place serves all three callers.
This commit is contained in:
@@ -180,9 +180,11 @@ func Input(ctx context.Context, r Reader, mesh Mesh, s Scope) (compose.Input, er
|
||||
return input(ctx, r, mesh, s, nil)
|
||||
}
|
||||
|
||||
// input is Input; with refuse set, a module placed on the composing node whose build cannot be read or taken in is
|
||||
// handed to refuse and composed as a stub (compose.Stub, kept for that node alone) rather than ending the read, so a
|
||||
// node's answer names every module it cannot compose (issue 1126).
|
||||
// input is Input; with refuse set, a module placed on the composing node whose build cannot be read or taken in, or whose
|
||||
// manifest the mesh refuses, is handed to refuse and composed as a stub (compose.Stub, kept for that node alone)
|
||||
// rather than ending the read, so a node's answer names every module it cannot compose (issue 1126, issue 774); a
|
||||
// module placed elsewhere whose manifest the mesh refuses is read for its offer alone (unusable). A manifest the bus does
|
||||
// not answer for still ends the read: that is the bus's, not the module's.
|
||||
func input(ctx context.Context, r Reader, mesh Mesh, s Scope, refuse func(module string, err error)) (compose.Input, error) {
|
||||
placements, err := r.Placements(ctx)
|
||||
if err != nil {
|
||||
@@ -248,14 +250,33 @@ func input(ctx context.Context, r Reader, mesh Mesh, s Scope, refuse func(module
|
||||
}
|
||||
return parse(raw)
|
||||
}
|
||||
ownPlaced := map[string]bool{}
|
||||
for _, pm := range placements[s.Node].Modules {
|
||||
ownPlaced[pm.Module] = true
|
||||
}
|
||||
for _, name := range sortedKeys(modules) {
|
||||
m := modules[name]
|
||||
if m.Manifest == "" {
|
||||
continue // a registered module the indexer has not read: nothing to compose it from
|
||||
}
|
||||
man, err := read(name, m.Manifest)
|
||||
raw, err := r.Manifest(ctx, m.Manifest)
|
||||
if err != nil {
|
||||
return compose.Input{}, err
|
||||
// Not read is no refusal of the module: a bus that does not answer is waited out by the caller.
|
||||
return compose.Input{}, fmt.Errorf("frombus: the manifest of %s (digest %s): %w", name, m.Manifest, err)
|
||||
}
|
||||
man, err := parse(raw)
|
||||
if err != nil {
|
||||
if refuse == nil {
|
||||
return compose.Input{}, err
|
||||
}
|
||||
// One module's manifest the mesh refuses refuses that module alone, by the rule a build that cannot be read
|
||||
// follows (issue 774: power's bar item refused every node's composition, 2026-10-30). It stands in as its
|
||||
// unjudged stub, so what it provides and requires still shapes the rest; it is held on the composing node
|
||||
// where it is placed there, and elsewhere only its offer is read.
|
||||
man = unusable(name, raw)
|
||||
if ownPlaced[name] {
|
||||
refuse(name, err)
|
||||
}
|
||||
}
|
||||
in.Catalogue[name] = man
|
||||
}
|
||||
@@ -530,6 +551,19 @@ func plansIn(in compose.Input, node string, mode compose.Mode) (map[string]catal
|
||||
return composed.Plans, nil
|
||||
}
|
||||
|
||||
// unusable is what stands in for a module's manifest the mesh refuses: its stub (compose.Stub)
|
||||
// read from its JSON unjudged, so what it provides, requires and serves still shapes the rest, and without its items,
|
||||
// since nothing judged what they would put into another module's collection. Bytes that are no manifest of the module
|
||||
// stand in as a stub that offers nothing.
|
||||
func unusable(module string, raw []byte) catalogue.Manifest {
|
||||
var m catalogue.Manifest
|
||||
if json.Unmarshal(raw, &m) != nil || m.Module != module {
|
||||
m = catalogue.Manifest{Module: module}
|
||||
}
|
||||
m.Items = nil
|
||||
return compose.Stub(m)
|
||||
}
|
||||
|
||||
// parser reads a manifest as the indexer does, against the mesh's predefined keys when given.
|
||||
func parser(mesh Mesh) func([]byte) (catalogue.Manifest, error) {
|
||||
return func(raw []byte) (catalogue.Manifest, error) {
|
||||
|
||||
@@ -12,7 +12,10 @@ import (
|
||||
// ADR 0570 clause 2 (issue 1954; a3's H5): the in-memory mesh composes a plan from the bus that hands restic to the
|
||||
// provisioner and names its postgres-database, provided by postgres on n1. And one bad module must not drop its node
|
||||
// (issue 774): with a module placed beside it whose built manifest cannot be used, or whose plan refuses, PlansHeld
|
||||
// still hands restic's plan and names the bad one with why, where Plans refuses the whole node.
|
||||
// still hands restic's plan and names the bad one with why, where Plans refuses the whole node. A manifest the indexer
|
||||
// holds that the mesh refuses (power's bar item with attributes its bar places by none of, mesh-catalog ca8d6f33,
|
||||
// 2026-10-30) holds that module alone on its node, in the plans and in the node's composition (Compose), and on a node
|
||||
// it is not placed on holds nothing.
|
||||
func TestTheProvisionedMeshHandsAPlanWithItsProvision(t *testing.T) {
|
||||
fp := "src2:" + strings.Repeat("9", 64)
|
||||
for _, row := range []struct {
|
||||
@@ -21,6 +24,8 @@ func TestTheProvisionedMeshHandsAPlanWithItsProvision(t *testing.T) {
|
||||
// built is the manifest backup's done build names; empty gives it no build
|
||||
built string
|
||||
why string // words backup's hold says
|
||||
// elsewhere leaves backup in the indexer's modules and places it on no node: nothing is held.
|
||||
elsewhere bool
|
||||
}{
|
||||
{name: "every module composes"},
|
||||
{name: "a built manifest that cannot be used",
|
||||
@@ -29,15 +34,19 @@ func TestTheProvisionedMeshHandsAPlanWithItsProvision(t *testing.T) {
|
||||
{name: "a plan that refuses",
|
||||
bad: `{"module":"backup","version":"1","resources":[{"id":"conf","type":"file","path":"/etc/backup.conf",` +
|
||||
`"mode":"0600","content":"key=${secret:nope}"}]}`, why: "${secret:nope}"},
|
||||
{name: "an indexer's manifest the mesh refuses", bad: unusableItem, why: `"shows" is none of them`},
|
||||
{name: "an indexer's manifest the mesh refuses, placed elsewhere", bad: unusableItem, elsewhere: true},
|
||||
} {
|
||||
b, err := Provisioned()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if row.bad != "" {
|
||||
p := b.Placement[Node]
|
||||
p.Modules = append(p.Modules, delivery.PlacedModule{Module: "backup"})
|
||||
b.Placement[Node] = p
|
||||
if !row.elsewhere {
|
||||
p := b.Placement[Node]
|
||||
p.Modules = append(p.Modules, delivery.PlacedModule{Module: "backup"})
|
||||
b.Placement[Node] = p
|
||||
}
|
||||
digest := delivery.ManifestDigest([]byte(row.bad))
|
||||
b.Manifests[digest] = []byte(row.bad)
|
||||
b.Module["backup"] = delivery.Module{FP2: fp, Manifest: digest}
|
||||
@@ -47,10 +56,27 @@ func TestTheProvisionedMeshHandsAPlanWithItsProvision(t *testing.T) {
|
||||
b.Builds["backup "+fp] = &delivery.BuilderBuild{State: delivery.BuildDone, Manifest: builtDigest,
|
||||
Artifacts: []delivery.BuiltArtifact{{Name: "files", Kind: "archive", Reference: "artifact-store://backup/files/blobs/x", Digest: "sha256:x"}}}
|
||||
}
|
||||
if _, err := frombus.Plans(context.Background(), b, frombus.Mesh{}, frombus.Scope{Node: Node}); err == nil {
|
||||
if _, err := frombus.Plans(context.Background(), b, frombus.Mesh{}, frombus.Scope{Node: Node}); err == nil && !row.elsewhere {
|
||||
t.Errorf("%s: Plans composed the node; the row does not refuse it", row.name)
|
||||
}
|
||||
}
|
||||
if row.why != "" || row.bad == "" {
|
||||
// The node's own composition holds by the same rule: every other module composed, the bad one named.
|
||||
fps := FPs()
|
||||
if row.bad != "" {
|
||||
fps["backup"] = fp
|
||||
}
|
||||
out, _, held, err := frombus.Compose(context.Background(), b, frombus.Mesh{}, frombus.Scope{Node: Node}, fps)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: the node's composition: %v", row.name, err)
|
||||
}
|
||||
if key, _ := delivery.ComposedKey(Node, "restic", fps["restic"]); out[key].Module != "restic" {
|
||||
t.Errorf("%s: restic is not composed (held %v)", row.name, held)
|
||||
}
|
||||
if why, ok := held["backup"]; (row.bad == "" && len(held) > 0) || (row.bad != "" && (!ok || !strings.Contains(why, row.why) || len(held) != 1)) {
|
||||
t.Errorf("%s: the node's composition held %v, want backup alone, saying %q", row.name, held, row.why)
|
||||
}
|
||||
}
|
||||
plans, held, err := frombus.PlansHeld(context.Background(), b, frombus.Mesh{}, frombus.Scope{Node: Node})
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", row.name, err)
|
||||
@@ -60,7 +86,7 @@ func TestTheProvisionedMeshHandsAPlanWithItsProvision(t *testing.T) {
|
||||
restic.Provisions[0].Provider != Node || restic.Provisions[0].ProviderModule != "postgres" {
|
||||
t.Fatalf("%s: the plans are %+v (held %v)", row.name, plans, held)
|
||||
}
|
||||
if row.bad == "" {
|
||||
if row.bad == "" || row.elsewhere {
|
||||
if len(held) > 0 {
|
||||
t.Errorf("%s: held %v", row.name, held)
|
||||
}
|
||||
@@ -74,3 +100,10 @@ func TestTheProvisionedMeshHandsAPlanWithItsProvision(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// unusableItem is a manifest the mesh refuses as the node-engine did power's at mesh-catalog ca8d6f33: a bar block item
|
||||
// with attributes its bar places by none of.
|
||||
const unusableItem = `{"module":"backup","version":"1","moved":true,` +
|
||||
`"build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]},` +
|
||||
`"items":[{"collection":"contribution:node-bar:block",` +
|
||||
`"attributes":{"bar":"bottom","place":"status","shows":"state","state":"power.draw"},"template":"files/bar"}]}`
|
||||
|
||||
Reference in New Issue
Block a user