From b60138990b341f5d125e46b03280bc54a72986c4 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 31 Oct 2026 01:05:01 +0100 Subject: [PATCH] Read a machine's placement once per answer of own, and say how long every answer took and every refusal, so an answer of 143 credentials is not 143 bus reads and an ask that went unanswered can be told from one never heard (issue 774) Quick fix: the plan reader keeps the machine's one placement key for the answer, as it keeps each module's assignment (issue 1126, 2026-10-28); the second time one answer's reads per credential held own past the node-engine's 15 s. Thorough fix (not here): the node-engine asks own only when a secret or a credential it composes from changed, and the assigner writes no want that is unchanged. --- modules/mesh-vault/cmd/mesh-vault/ownnode.go | 41 ++++++++++++++++--- .../mesh-vault/cmd/mesh-vault/ownnode_test.go | 16 ++++++++ modules/mesh-vault/cmd/mesh-vault/placed.go | 17 +++++--- 3 files changed, 64 insertions(+), 10 deletions(-) diff --git a/modules/mesh-vault/cmd/mesh-vault/ownnode.go b/modules/mesh-vault/cmd/mesh-vault/ownnode.go index d1af6800..648fc6fe 100644 --- a/modules/mesh-vault/cmd/mesh-vault/ownnode.go +++ b/modules/mesh-vault/cmd/mesh-vault/ownnode.go @@ -11,8 +11,10 @@ import ( "encoding/json" "errors" "fmt" + "slices" "sort" "strings" + "time" stdio "git.novox.be/novox/mesh-sdk/go" "git.novox.be/novox/mesh-sdk/go/catalogue" @@ -87,7 +89,7 @@ func (v *Vault) credentialRefusal(node string, c CredentialAsked, plan *planRead } // The consuming module stands on the machine: read from the placement and the assignments, never assumed (one // that cannot be read refuses). The assignments' mirror can be behind, so one it does not hold is confirmed directly. - placed, err := v.placedOn(node, c.ConsumerModule) + placed, err := plan.placedOn(c.ConsumerModule) if err != nil { return fmt.Sprintf(notYetPrefix+"where %s stands could not be read: %v", c.ConsumerModule, err) } @@ -122,13 +124,19 @@ func (v *Vault) credentialRefusal(node string, c CredentialAsked, plan *planRead } // planReader reads the assignments of one machine's modules for one answer of `own`, each module's once (issue 1126, -// 2026-10-28: one read per own secret and per credential, 203 on novox, held the answer past the node-engine's bound). +// 2026-10-28: one read per own secret and per credential, 203 on novox, held the answer past the node-engine's bound), +// and the machine's placement once (issue 774, 2026-10-31: read once per credential, 143 bus reads in one answer to ace). type planReader struct { node string reader AssignmentReader confirm AssignmentReader why string got map[string]planRead + // placement reads the machine's placed modules; placed and placedErr keep the one read for the answer. + placement func() ([]string, error) + placedRead bool + placed []string + placedErr error } type planRead struct { @@ -142,7 +150,17 @@ func (v *Vault) plansOf(node string) *planReader { if d, ok := v.Askers.(Declared); ok && d.Confirm != nil { confirm = standing(d.Confirm) } - return &planReader{node: node, confirm: confirm, reader: read, why: why, got: map[string]planRead{}} + return &planReader{node: node, confirm: confirm, reader: read, why: why, got: map[string]planRead{}, + placement: func() ([]string, error) { return v.placedModules(node) }} +} + +// placedOn says module is placed on the machine by the catalogue, from the machine's placement read once for the answer. +func (p *planReader) placedOn(module string) (bool, error) { + if !p.placedRead { + p.placed, p.placedErr = p.placement() + p.placedRead = true + } + return slices.Contains(p.placed, module), p.placedErr } // read is module's assignment on the machine, nil when there is none, or why the vault reads none. @@ -222,7 +240,22 @@ func composeOrigin(origin string) string { } // OwnOfNode answers the node-engine of a machine the own secrets of its modules, each sealed to the machine's stated key. +// Every answer says how long it took, and a refusal is said too (issue 774, 2026-10-31: ace's asks went unanswered past +// the node-engine's 15 s and the vault's journal named none of them, so whether they arrived could not be told). func (v *Vault) OwnOfNode(caller string, asked ...CredentialAsked) (OwnAnswer, error) { + began := v.Now() + a, err := v.ownOfNode(caller, asked...) + took := v.Now().Sub(began).Round(time.Millisecond) + if err != nil { + v.log("own for %s, %d credentials asked, refused after %s: %v", caller, len(asked), took, err) + return a, err + } + v.log("handed %s's node-engine the %d own secrets of its modules and %d credentials (%d not held), sealed to its key, in %s", + a.Node, len(a.Own), len(a.Credentials), len(a.Missing), took) + return a, nil +} + +func (v *Vault) ownOfNode(caller string, asked ...CredentialAsked) (OwnAnswer, error) { node, ok := nodeEngineOf(caller) if !ok { if caller == "" || caller == stdio.Unproven { @@ -328,8 +361,6 @@ func (v *Vault) OwnOfNode(caller string, asked ...CredentialAsked) (OwnAnswer, e return OwnAnswer{}, fmt.Errorf("nothing is handed out to %s: who was handed its %d values could not be recorded: %w", caller, len(handings), err) } - v.log("handed %s's node-engine the %d own secrets of its modules and %d credentials (%d not held), sealed to its key", - node, len(out.Own), len(out.Credentials), len(out.Missing)) return out, nil } diff --git a/modules/mesh-vault/cmd/mesh-vault/ownnode_test.go b/modules/mesh-vault/cmd/mesh-vault/ownnode_test.go index ad21bb31..7a957ff1 100644 --- a/modules/mesh-vault/cmd/mesh-vault/ownnode_test.go +++ b/modules/mesh-vault/cmd/mesh-vault/ownnode_test.go @@ -2,6 +2,7 @@ package main import ( "errors" + "fmt" "os" "path/filepath" "strings" @@ -143,6 +144,11 @@ func TestOwnReadsEachAssignmentOnceAndSyncsItsRecordOnce(t *testing.T) { t.Fatal(err) } } + // Issue 774, 2026-10-31 (ace's 143 credentials went unanswered past the node-engine's 15 s while novox's asked every + // 8 s): the machine's placement, one key, is read once per answer too, never once per credential. + placed := placementsOf(t, map[string][]string{"anchor": {"web"}}) + placementReads := 0 + v.Placement = PlacementReader{Keys: placed.Keys, Read: func(key string) ([]byte, error) { placementReads++; return placed.Read(key) }} for k := range reads { delete(reads, k) } @@ -161,6 +167,10 @@ func TestOwnReadsEachAssignmentOnceAndSyncsItsRecordOnce(t *testing.T) { if len(reads) != 2 { t.Errorf("the answer read %v, want each of anchor's two modules once", reads) } + if placementReads != 1 { + t.Errorf("one answer of two credentials read anchor's placement %d times, want once", placementReads) + } + v.Placement = PlacementReader{} if n := v.Store.syncs - syncs; n != 1 { t.Errorf("one answer synced the record %d times", n) } @@ -182,9 +192,15 @@ func TestOwnReadsEachAssignmentOnceAndSyncsItsRecordOnce(t *testing.T) { if err := os.Mkdir(filepath.Join(v.dir, "store", "handed.jsonl"), 0o700); err != nil { t.Fatal(err) } + // And the refusal is said in the vault's journal with how long it took, as every answer is. + var said []string + v.Log = func(format string, args ...any) { said = append(said, fmt.Sprintf(format, args...)) } if a, err := v.OwnOfNode("node.anchor"); err == nil { t.Errorf("handed with no record of it: %+v", a) } + if last := strings.Join(said, "\n"); !strings.Contains(last, "own for node.anchor, 0 credentials asked, refused after ") { + t.Errorf("a refused own was said as %q", last) + } } func mustHanded(t *testing.T, v testVault) []Handing { diff --git a/modules/mesh-vault/cmd/mesh-vault/placed.go b/modules/mesh-vault/cmd/mesh-vault/placed.go index 8e00f828..d7ca91a5 100644 --- a/modules/mesh-vault/cmd/mesh-vault/placed.go +++ b/modules/mesh-vault/cmd/mesh-vault/placed.go @@ -58,22 +58,29 @@ func (v *Vault) placements() (map[string]delivery.Placement, error) { // placedOn says module is placed on node by the catalogue. func (v *Vault) placedOn(node, module string) (bool, error) { + placed, err := v.placedModules(node) + return slices.Contains(placed, module), err +} + +// placedModules is every module the catalogue places on node, read from the node's one key of the placement; none when +// the vault reads no placement or the node has no key. +func (v *Vault) placedModules(node string) ([]string, error) { if v.Placement.Read == nil { - return false, nil + return nil, nil } key, err := delivery.PlacementKey(node) if err != nil { - return false, err + return nil, err } raw, err := v.Placement.Read(key) if err != nil || raw == nil { - return false, err + return nil, err } p, err := delivery.DecodePlacement(raw) if err != nil { - return false, fmt.Errorf("the placement of %s is not readable: %w", node, err) + return nil, fmt.Errorf("the placement of %s is not readable: %w", node, err) } - return slices.Contains(p.Assigned(), module), nil + return p.Assigned(), nil } // standsOn says module stands on node: placed there, or assigned there while the assignments stand. A read that fails is -- 2.54.0