Builder: the start's sweep keeps a key of a fingerprint a followed half's picture holds

The follower closes Ready before it tells its entries, so the sweep at ready could close a building key
whose entry was not taken in yet and the entry then asked again (four asks where two). The sweep now
takes the fingerprints of the halves' pictures as current.

Builds: design 47 G §348, §479 (the builder reads the catalog half per watched branch), issue 1660's sweep
Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-12 01:02:27 +01:00
parent 3a2b6f4a9c
commit e1bf32b9fb
4 changed files with 52 additions and 2 deletions
@@ -10,6 +10,7 @@ import (
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/views"
"mesh-delivery-builder/internal/build"
@@ -184,6 +185,29 @@ func (c *catalogHalf) dropped(state string) {
c.checkReady()
}
// pictured are the build keys of the fingerprints the adopted halves' pictures hold, whether or not their entries were told yet.
func (c *catalogHalf) pictured() []string {
c.mu.Lock()
halves := make([]*views.Follower, 0, len(c.halves))
for _, f := range c.halves {
halves = append(halves, f)
}
c.mu.Unlock()
var keys []string
for _, f := range halves {
for module, raw := range f.Values() {
var e meshcatalog.CatalogEntry
if json.Unmarshal(raw, &e) != nil || e.Fp == "" {
continue
}
if k, err := builds.BuilderBuildKey(module, e.Fp); err == nil {
keys = append(keys, k)
}
}
}
return keys
}
// checkReady calls ready once every adopted half's first view is in.
func (c *catalogHalf) checkReady() {
c.mu.Lock()
@@ -433,7 +433,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
// a holder of the seat that is down holds no other part of it).
half.b = b
half.ready = func() {
if err := b.Sweep(ctx); err != nil {
if err := b.Sweep(ctx, half.pictured()...); err != nil {
errs <- err
}
}
@@ -46,3 +46,22 @@ func TestSweepLeavesAKeyOfTheModulesCurrentFingerprintForTheBuildToTakeOver(t *t
t.Errorf("build = %+v, want done by novox.builder", v)
}
}
// A key of a fingerprint a followed half's picture holds is not closed at start, though its entry is not taken in yet: the
// follower is ready before it tells its entries (design 47 G A-107's restart case: the builder put its asks a second time).
func TestSweepLeavesAKeyOfAPicturedFingerprintBuildingBeforeItsEntryIsTakenIn(t *testing.T) {
r := newRig(t, goBundle)
seed(t, r.st, builds.BuilderBuild{State: builds.BuildBuilding, By: "ace.builder", Manifest: manifestD})
if err := r.b.Sweep(context.Background(), r.key); err != nil {
t.Fatal(err)
}
if v := valueOf(t, r.st, r.key); v.State != builds.BuildBuilding {
t.Errorf("build = %+v, want it left building for the entry to take over", v)
}
if err := r.b.Sweep(context.Background()); err != nil {
t.Fatal(err)
}
if v := valueOf(t, r.st, r.key); v.State != builds.BuildFailed {
t.Errorf("without a picture naming it the key is closed: %+v", v)
}
}
@@ -321,7 +321,11 @@ func (b *Builder) NodesChanged(ctx context.Context) { b.Reconsider(ctx) }
// module no longer has (issue 1660, ADR 0478 clause 6): the key can never be built, since nothing asks for it, and would
// stay building for ever. It fails the key naming who left it. A key of the fingerprint the module has now is not
// touched: the build takes it over. Call it once the modules bucket has been read, so every module is known.
func (b *Builder) Sweep(ctx context.Context) error {
//
// pictured are the build keys the followed halves' pictures name as current: a follower is ready before it has told its
// entries (views.Follower closes Ready, then calls Changed), so at ready the entries may not be taken in yet, and a key of
// a fingerprint a picture holds is never closed for want of the entry's arrival (the restart that put its asks a second time).
func (b *Builder) Sweep(ctx context.Context, pictured ...string) error {
b.init()
keys, err := b.Claimer.Store.Keys(ctx)
if err != nil {
@@ -329,6 +333,9 @@ func (b *Builder) Sweep(ctx context.Context) error {
}
b.mu.Lock()
current := map[string]bool{}
for _, k := range pictured {
current[k] = true
}
// What every followed branch carries now is current, not only the first branch's (design 47 G §348).
for _, held := range []map[string]meshcatalog.ModulesByModuleValue{b.known, b.heard} {
for hk, m := range held {