indexer: the findings registration-history-filling past 80 % and seed-waits kept until the warrant

Builds: design 47 G §704 (registration-history, seed findings), §8

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-11 22:49:57 +01:00
parent e089fd9c3c
commit 0f62358fb0
5 changed files with 128 additions and 4 deletions
@@ -450,7 +450,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
registry := newRegistry(js, conn, cred, calls.Call,
awaitedStore{b: process.AwaitBucket(ctx, js, conn, index.RegistrationsBucket, "no registration is kept or written", logger.Printf)},
awaitedStore{b: process.AwaitBucket(ctx, js, conn, index.ProposalsBucket, "no change of a registration waits for its warrant", logger.Printf)},
routerRecordUnread, logger.Printf)
routerRecordUnread, ix, logger.Printf)
ix.Registry = registry
ix.SeedModules = func(context.Context) ([]index.SeedModule, error) {
return nil, errors.New("the first registrations are not composed by this build: what the build seats answer steps with is not read yet (design question)")
@@ -48,7 +48,7 @@ func historySubject(module string) string {
// newRegistry is the Registry over the indexer's own states, the stream registration-history, the seat git, and the seat
// operator-channel's accepts. where is the read of the router's own record of an ask (design question D-37, open).
func newRegistry(js jetstream.JetStream, conn *nats.Conn, cred process.Account, call toolCaller, registrations, proposals index.Store, where func(id string) (string, *oc.Warrant, error), logf func(string, ...any)) *index.Registry {
func newRegistry(js jetstream.JetStream, conn *nats.Conn, cred process.Account, call toolCaller, registrations, proposals index.Store, where func(id string) (string, *oc.Warrant, error), ix *index.Indexer, logf func(string, ...any)) *index.Registry {
return &index.Registry{Registrations: registrations, Proposals: proposals,
History: func(ctx context.Context, r meshcatalog.Registration) error {
data, err := json.Marshal(r)
@@ -56,6 +56,14 @@ func newRegistry(js jetstream.JetStream, conn *nats.Conn, cred process.Account,
return err
}
_, err = js.Publish(ctx, historySubject(r.Module), data, jetstream.WithMsgID(fmt.Sprintf("%s.%d", r.Module, r.Version)))
if err == nil {
// Past 80 % full the stream's fill is a finding (design 47 G §704).
if st, serr := js.Stream(ctx, catalogue.StreamBusName(index.RegistrationHistoryStream)); serr == nil {
if info, ierr := st.Info(ctx); ierr == nil {
ix.HistoryFill(ctx, int64(info.State.Bytes), info.Config.MaxBytes)
}
}
}
return err
},
HistoryOf: func(ctx context.Context, module string) ([]meshcatalog.Registration, error) {
@@ -107,8 +115,9 @@ func newRegistry(js jetstream.JetStream, conn *nats.Conn, cred process.Account,
if waiting {
logf("seed-waits: the first registrations wait on the operator (ask %s); indexer_seed_again asks them again", ask)
} else {
logf("seed-waits is cleared: the first registrations are asked or written (ask %s)", ask)
logf("seed-waits is cleared: the first registrations are written (ask %s)", ask)
}
ix.SeedWaits(context.Background(), ask, waiting)
},
Logf: logf}
}
@@ -0,0 +1,66 @@
package index
import (
"context"
"fmt"
mcc "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
)
// The findings of the registrations (design 47 G §704, §8): the stream registration-history past 80 % full, and the seed
// that waits for the operator.
const (
// HistoryFillingKind and HistoryFillingID are the finding of registration-history past HistoryFillingPercent (scope seat).
HistoryFillingKind = "registration-history-filling"
HistoryFillingID = "mesh-catalog.registration-history"
// HistoryFillingPercent is the fill at which the finding is written.
HistoryFillingPercent = 80
// SeedWaitsKind and SeedWaitsID are the finding of a seed that waits for a warrant (scope seat), cleared by the warrant
// and kept after a Decline or an expiry (§8).
SeedWaitsKind = "seed-waits"
SeedWaitsID = "mesh-catalog.seed"
)
// HistoryFill writes the finding registration-history-filling when the stream holds used bytes of its max, 80 % or more,
// and clears it under that; a stream with no cap says nothing (design 47 G §704).
func (ix *Indexer) HistoryFill(ctx context.Context, used, max int64) {
if ix.Findings == nil || max <= 0 {
return
}
if used*100 >= max*HistoryFillingPercent {
f := ix.observed(mcc.ScopeSeat, HistoryFillingID, HistoryFillingKind,
fmt.Sprintf("registration-history holds %d of %d bytes", used, max),
"The registration history is filling",
fmt.Sprintf("The stream registration-history keeps every version of every registration and refuses a new one when full, so a registration change whose version cannot be kept fails. It holds %d of its %d bytes.", used, max),
"indexer."+HistoryFillingKind)
if err := ix.Findings.Write(ctx, f); err != nil {
ix.logf("registrations: the finding %s is not written: %v", HistoryFillingKind, err)
}
return
}
if err := ix.clearFinding(ctx, mcc.ScopeSeat, HistoryFillingID, HistoryFillingKind, "registration-history is under 80 % full"); err != nil {
ix.logf("registrations: the finding %s is not cleared: %v", HistoryFillingKind, err)
}
}
// SeedWaits writes the finding seed-waits naming the ask when the first registrations wait, and clears it when the seed is
// asked or written (design 47 G §704, §8).
func (ix *Indexer) SeedWaits(ctx context.Context, ask string, waiting bool) {
if ix.Findings == nil {
return
}
if waiting {
f := ix.observed(mcc.ScopeSeat, SeedWaitsID, SeedWaitsKind,
"the first registrations wait on the operator (ask "+ask+")",
"The first registrations wait for the operator",
"The ask "+ask+" carries the first registrations and ended without a warrant; "+SeedAgainTool+" raises it again.",
"indexer."+SeedWaitsKind)
if err := ix.Findings.Write(ctx, f); err != nil {
ix.logf("registrations: the finding %s is not written: %v", SeedWaitsKind, err)
}
return
}
if err := ix.clearFinding(ctx, mcc.ScopeSeat, SeedWaitsID, SeedWaitsKind, "the first registrations are asked or written (ask "+ask+")"); err != nil {
ix.logf("registrations: the finding %s is not cleared: %v", SeedWaitsKind, err)
}
}
@@ -0,0 +1,48 @@
package index
import (
"context"
"testing"
mcc "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
)
// Design 47 G §704: registration-history past 80 % full is the finding registration-history-filling (scope seat, id
// mesh-catalog.registration-history), cleared under it; the seed that waits is seed-waits (scope seat, id mesh-catalog.seed),
// cleared by the warrant only.
func TestRegistrationFindings(t *testing.T) {
ctx := context.Background()
w := newFindings()
ix := &Indexer{Findings: w}
held := func(kind string) bool {
for _, f := range w.Held() {
if f.Scope == mcc.ScopeSeat && f.Kind == kind {
return true
}
}
return false
}
ix.HistoryFill(ctx, 79, 100)
if held(HistoryFillingKind) {
t.Fatal("79 % full wrote the finding")
}
ix.HistoryFill(ctx, 80, 100)
if !held(HistoryFillingKind) {
t.Fatal("80 % full did not write registration-history-filling")
}
if f := w.Held()[0]; f.ID != "mesh-catalog.registration-history" {
t.Errorf("the finding's id is %q", f.ID)
}
ix.HistoryFill(ctx, 10, 100)
if held(HistoryFillingKind) {
t.Fatal("the finding stands under 80 %")
}
ix.SeedWaits(ctx, "ask-1", true)
if !held(SeedWaitsKind) {
t.Fatal("a seed that waits wrote no finding")
}
ix.SeedWaits(ctx, "ask-1", false)
if held(SeedWaitsKind) {
t.Fatal("the warrant did not clear seed-waits")
}
}
@@ -146,7 +146,8 @@ func (r *Registry) ProposeSeed(ctx context.Context, modules []SeedModule, again
return "", err
}
if r.SeedWaits != nil {
r.SeedWaits(p.Ask, false)
// The seed waits on the operator while its ask is open; only the warrant clears the finding (design 47 G §704).
r.SeedWaits(p.Ask, true)
}
return p.Ask, nil
}