G §614: the node-engine writes the finding entry-not-found for a fingerprint the catalog half holds none of
Builds: design 47 G A-117, §614, A-15, A-95 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
@@ -620,6 +620,11 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, trus
|
||||
marks := newMarker(run)
|
||||
heldFindings := &seatwait.HeldKeeper{Node: node, Observer: findingsModule, Writer: findings, Ctx: run, Say: say}
|
||||
hooks := composerHooks{
|
||||
entryNotFound: func(fp string, notFound bool) {
|
||||
if err := seatwait.SayEntryNotFound(run, findings, node, findingsModule, fp, notFound, time.Now); err != nil {
|
||||
say("the finding that the catalog half holds no entry of " + fp + " cannot be kept: " + err.Error())
|
||||
}
|
||||
},
|
||||
seatList: seats,
|
||||
seatListFailed: err != nil,
|
||||
running: func() []compositions.Composed {
|
||||
@@ -1111,7 +1116,9 @@ type composerHooks struct {
|
||||
unseal func(string) ([]byte, error)
|
||||
refused func(why string, held map[string]string)
|
||||
kept func(kept map[string][]string)
|
||||
held func(module string) (compositions.Seals, []string, error)
|
||||
// entryNotFound is selfcompose.Env's EntryNotFound.
|
||||
entryNotFound func(fp string, notFound bool)
|
||||
held func(module string) (compositions.Seals, []string, error)
|
||||
// vaultAuthority and certify are selfcompose.Env's VaultAuthority and Certify.
|
||||
vaultAuthority func() string
|
||||
certify func(ctx context.Context) error
|
||||
@@ -1173,7 +1180,7 @@ func composerEnv(session *link.Session, node string, say func(string), hooks com
|
||||
Hear: session.Hear,
|
||||
// And a moved module's templates archive, in the artifact store, as every archive is fetched (issue 1126).
|
||||
Fetch: apply.Fetch,
|
||||
Running: hooks.running, Unseal: hooks.unseal, Refused: hooks.refused, Held: hooks.held, Kept: hooks.kept,
|
||||
Running: hooks.running, Unseal: hooks.unseal, Refused: hooks.refused, Held: hooks.held, Kept: hooks.kept, EntryNotFound: hooks.entryNotFound,
|
||||
VaultAuthority: hooks.vaultAuthority, Certify: hooks.certify}
|
||||
if hooks.seatList != nil {
|
||||
env.SeatList, env.SeatListChanged = hooks.seatList.reader, hooks.seatList.changed
|
||||
|
||||
@@ -125,7 +125,7 @@ func newFindingsOfTheNode(session *link.Session, node string, say func(string))
|
||||
Module: findingsModule,
|
||||
Node: node,
|
||||
State: link.FindingsState{Session: session},
|
||||
Kinds: []string{seatwait.Kind, seatwait.HeldKind},
|
||||
Kinds: []string{seatwait.Kind, seatwait.HeldKind, seatwait.EntryNotFoundKind},
|
||||
Asked: condseat.SubjectFindingsAsked,
|
||||
Logf: func(format string, args ...any) { say(fmt.Sprintf(format, args...)) },
|
||||
Publish: session.PublishEvent,
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package seatwait
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
condseat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
|
||||
"git.novox.be/novox/mesh-sdk/go/keys"
|
||||
)
|
||||
|
||||
// EntryNotFoundKind is the finding a part writes when the catalog half holds no entry of a fingerprint the part holds a want or a
|
||||
// running for (design 47 G §614, A-15, A-117): scope module, its id the fingerprint escaped as a name.
|
||||
const EntryNotFoundKind = "entry-not-found"
|
||||
|
||||
// EntryNotFoundID is the finding's id for a fingerprint.
|
||||
func EntryNotFoundID(fp string) string { return keys.EscapeName(fp) }
|
||||
|
||||
// SayEntryNotFound writes the finding for fp when notFound, and clears it when no longer: the entry answered later, or nothing the
|
||||
// node holds names the fingerprint any more. A write or a clear that fails is returned for the caller to say.
|
||||
func SayEntryNotFound(ctx context.Context, w Writer, node, observer, fp string, notFound bool, now func() time.Time) error {
|
||||
id := EntryNotFoundID(fp)
|
||||
if !notFound {
|
||||
return w.Clear(ctx, scopeModule, id, EntryNotFoundKind, "the entry is answered, or nothing the node holds names its fingerprint")
|
||||
}
|
||||
return w.Write(ctx, condseat.Finding{Observer: observer, Kind: EntryNotFoundKind, Scope: scopeModule, ID: id,
|
||||
Severity: condseat.FindingSeverityWarning, Source: EntryNotFoundKind,
|
||||
Subject: condseat.Subject{Scope: scopeModule, ID: observer, Machine: node},
|
||||
Summary: fmt.Sprintf("the catalog half holds no entry of the build %s, so %s composes nothing new of it", fp, node),
|
||||
Headline: fmt.Sprintf("no entry of the build %s on %s", fp, node),
|
||||
Explanation: "A want or a running names this build and the catalog half answered that it holds no entry of it. The finding clears when the entry is answered or nothing the node holds names the build.",
|
||||
At: now()})
|
||||
}
|
||||
@@ -3,10 +3,12 @@ package seatwait
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
condseat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
|
||||
"git.novox.be/novox/mesh-sdk/go/keys"
|
||||
"git.novox.be/novox/mesh-sdk/go/seatlist"
|
||||
)
|
||||
|
||||
@@ -214,6 +216,22 @@ func caseAHeldModuleIsAFindingClearedWhenItIsComposedOrNoLongerWanted(t *testing
|
||||
}
|
||||
}
|
||||
|
||||
// Design 47 G §614, A-117: the finding entry-not-found is of scope module with the fingerprint, escaped, as its id, and cleared by it.
|
||||
func caseAnEntryNotFoundIsAFindingOfTheFingerprint(t *testing.T) {
|
||||
w := &fakeFindings{}
|
||||
fp := "src2:" + strings.Repeat("a", 64)
|
||||
now := func() time.Time { return time.Unix(1, 0) }
|
||||
if err := SayEntryNotFound(context.Background(), w, "n1", "mesh-host", fp, true, now); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(w.put) != 1 || w.put[0].Kind != EntryNotFoundKind || w.put[0].Scope != "module" || w.put[0].ID != keys.EscapeName(fp) || strings.Contains(w.put[0].ID, ":") {
|
||||
t.Fatalf("written %+v", w.put)
|
||||
}
|
||||
if err := SayEntryNotFound(context.Background(), w, "n1", "mesh-host", fp, false, now); err != nil || len(w.cleared) != 1 || w.cleared[0].ID != w.put[0].ID {
|
||||
t.Fatalf("cleared %+v, %v", w.cleared, err)
|
||||
}
|
||||
}
|
||||
|
||||
// ADR 0636 §1, the node's finding of a seat list unread.
|
||||
func TestTheSeatListWait(t *testing.T) {
|
||||
for name, run := range map[string]func(*testing.T){
|
||||
@@ -221,6 +239,7 @@ func TestTheSeatListWait(t *testing.T) {
|
||||
"a read within the bound writes nothing": caseAReadWithinTheBoundWritesNothing,
|
||||
"a finding that cannot be written is said and tried again": caseAFindingThatCannotBeWrittenIsSaidAndTriedAgain,
|
||||
"a finding of an earlier process is cleared at the first full read": caseAFindingOfAnEarlierProcessIsClearedAtTheFirstFullRead,
|
||||
"an entry not found is a finding of the fingerprint": caseAnEntryNotFoundIsAFindingOfTheFingerprint,
|
||||
"a held module is a finding cleared when it is composed or no longer wanted": caseAHeldModuleIsAFindingClearedWhenItIsComposedOrNoLongerWanted,
|
||||
} {
|
||||
t.Run(name, run)
|
||||
|
||||
@@ -153,9 +153,32 @@ func (c *Composer) entryChanged(row meshcatalogclient.EntryRow) {
|
||||
c.mu.Unlock()
|
||||
c.keep(entryKey(row.Fingerprint), meshcatalog.EntryAnswer{Entry: *row.Entry})
|
||||
}
|
||||
c.tellNotFound(row.Fingerprint, row.State == meshcatalogclient.EntryNotFound)
|
||||
c.askRehand()
|
||||
}
|
||||
|
||||
// tellNotFound tells the finding entry-not-found once when fp's entry is not found, and once when it no longer is (design 47 G §614, A-117).
|
||||
func (c *Composer) tellNotFound(fp string, notFound bool) {
|
||||
c.mu.Lock()
|
||||
was := c.notFound[fp]
|
||||
if was == notFound {
|
||||
c.mu.Unlock()
|
||||
return
|
||||
}
|
||||
if c.notFound == nil {
|
||||
c.notFound = map[string]bool{}
|
||||
}
|
||||
if notFound {
|
||||
c.notFound[fp] = true
|
||||
} else {
|
||||
delete(c.notFound, fp)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
if c.env.EntryNotFound != nil {
|
||||
c.env.EntryNotFound(fp, notFound)
|
||||
}
|
||||
}
|
||||
|
||||
// keep writes one value into the composition state; a state that cannot be written is said, and the value is kept again by
|
||||
// the next pass.
|
||||
func (c *Composer) keep(key string, v any) {
|
||||
@@ -236,6 +259,7 @@ func (c *Composer) syncEntries(ctx context.Context) {
|
||||
for _, row := range c.entries.Rows() {
|
||||
if _, named := need[row.Fingerprint]; !named {
|
||||
c.entries.Release(row.Fingerprint)
|
||||
c.tellNotFound(row.Fingerprint, false)
|
||||
}
|
||||
}
|
||||
c.mu.Lock()
|
||||
|
||||
@@ -58,6 +58,8 @@ func composeFromEntries(t *testing.T) {
|
||||
env := b.env()
|
||||
env.Entry, env.ComposedState = EntryCall(ask), state
|
||||
env.Refused = func(_ string, h map[string]string) { mu.Lock(); held = h; mu.Unlock() }
|
||||
told := map[string]bool{}
|
||||
env.EntryNotFound = func(fp string, notFound bool) { mu.Lock(); told[fp] = notFound; mu.Unlock() }
|
||||
c := New(env)
|
||||
out, err := c.Watch(ctx, "n1.>")
|
||||
if err != nil {
|
||||
@@ -97,6 +99,13 @@ func composeFromEntries(t *testing.T) {
|
||||
if !strings.HasPrefix(why, agent.EntryMissing) || !strings.Contains(why, ghost) || held["lemurs"] != "" {
|
||||
t.Errorf("held %q, want ghost held with %s naming %s", held, agent.EntryMissing, ghost)
|
||||
}
|
||||
// And the finding entry-not-found is told for the fingerprint the half holds none of, and for no other (design 47 G §614, A-117).
|
||||
mu.Lock()
|
||||
nf, nfOther := told[ghost], told[fp]
|
||||
mu.Unlock()
|
||||
if !nf || nfOther {
|
||||
t.Errorf("entry-not-found told %v for the ghost and %v for the entry held", nf, nfOther)
|
||||
}
|
||||
// Each entry and want it composes from is kept in the composition state before the composition that uses them.
|
||||
state.mu.Lock()
|
||||
_, hasEntry := state.kv[entryKey(fp)]
|
||||
|
||||
@@ -61,6 +61,9 @@ const ResolvedBucket = "mesh-config_resolved"
|
||||
// Env is what the composer needs of the node.
|
||||
type Env struct {
|
||||
Node string
|
||||
// EntryNotFound is told when the catalog half answered it holds no entry of a fingerprint (notFound), and when that is no longer so:
|
||||
// the entry answered, or nothing the node holds names it (design 47 G §614, A-117). Nil tells nobody.
|
||||
EntryNotFound func(fp string, notFound bool)
|
||||
// Watch follows one bucket's keys matching pattern, as the agent's watches do.
|
||||
Watch func(ctx context.Context, bucket, pattern string) (<-chan agent.Update, error)
|
||||
// Get reads one key's latest value; nil and no error when there is none.
|
||||
@@ -182,8 +185,10 @@ type Composer struct {
|
||||
// entries is the cache of entries asked by fingerprint, nil where the composer reads the indexer's states; keptEnt the entries
|
||||
// held from the composition state or an answer, keptWantVal the wants it keeps there and keptWants the modules whose want
|
||||
// stands from that state alone until the live read of the wants answers (entries.go). Under mu; keptWrite orders the writes.
|
||||
entries *meshcatalogclient.Entries
|
||||
keptEnt map[string]keptEntry
|
||||
entries *meshcatalogclient.Entries
|
||||
keptEnt map[string]keptEntry
|
||||
// notFound is each fingerprint whose entry-not-found the composer told, so it is told once and cleared once.
|
||||
notFound map[string]bool
|
||||
keptWantVal map[string]keptWant
|
||||
keptWants map[string]bool
|
||||
keptWrite sync.Mutex
|
||||
|
||||
Reference in New Issue
Block a user