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