The first read of a chain not laid out lays it out, so no read of the start is refused (issue 1121)
mesh/merge-gate head a825a4c90446 merged into trunk d3556f452e8c — pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every …
mesh/repo-check head a825a4c90446 merged into trunk d3556f452e8c — pass: its merge-check.sh passed
mesh/delivery delivered

The layout gate is switched on when the stores open (UseChainOpener), before
attachTheChain; a read of the start came first, was refused, and the
controller exited. A read that finds no marker now lays the builds out once,
under a lock, and is refused only when that fails (a process without the
lease).
This commit is contained in:
2026-10-18 14:39:43 +02:00
parent 315c622386
commit a825a4c904
3 changed files with 50 additions and 9 deletions
+17 -1
View File
@@ -41,6 +41,16 @@ func (i *Inventory) RequireLayout() {
i.state.mu.Unlock()
}
// layingOut lets one read at a time lay the builds out; the others wait for it and then find the marker.
var layingOut sync.Mutex
// layOutOnce lays the builds out under their modules unless another read did so meanwhile.
func (i *Inventory) layOutOnce(ctx context.Context) error {
layingOut.Lock()
defer layingOut.Unlock()
return i.LayBuildsOut(ctx, func(format string, args ...any) { fmt.Printf("chain: "+format+"\n", args...) })
}
func (i *Inventory) markLaidOut() {
i.state.mu.Lock()
i.state.laid = true
@@ -107,7 +117,13 @@ func (i *Inventory) theChain(ctx context.Context) (*chain.Chain, error) {
if _, found, err := c.Builds.GetRaw(ctx, chain.LayoutMarkerKey); err != nil {
return nil, err
} else if !found {
return nil, ErrNotLaidOut
// **The first read lays them out** (issue 1121): on 2026-10-18 a read of the controller's start came before
// the layout and was refused, and the controller exited and was started again every five seconds. A
// process that may write (the lease holder) lays them out here, once; one that may not is refused, loudly.
if err := i.layOutOnce(ctx); err != nil {
return nil, fmt.Errorf("%w (laying them out now failed: %v)", ErrNotLaidOut, err)
}
return c, nil
}
i.markLaidOut()
}
@@ -3,7 +3,6 @@ package inventory
import (
"context"
"encoding/json"
"errors"
"fmt"
"reflect"
"sort"
@@ -398,9 +397,10 @@ func TestAnOlderBuildFinishingLastLeavesThePointersOnTheNewer(t *testing.T) {
}
}
// Nothing reads the chain on the bus until the controller's start has laid the builds out and written the marker; the layout
// is done once.
func TestTheChainIsRefusedUntilTheBuildsAreLaidOutAndTheLayoutIsDoneOnce(t *testing.T) {
// Nothing reads the chain on the bus before the builds are laid out and the marker written: the first read of a process that
// may write lays them out itself (issue 1121: a refused read of the start made the controller exit), and the layout is done
// once.
func TestTheFirstReadLaysTheBuildsOutAndTheLayoutIsDoneOnce(t *testing.T) {
mem := chain.NewMemory()
inv, c := layoutChain(mem)
inv.RequireLayout()
@@ -409,11 +409,11 @@ func TestTheChainIsRefusedUntilTheBuildsAreLaidOutAndTheLayoutIsDoneOnce(t *test
if _, err := c.Builds.Create(ctx, old.ID, old); err != nil {
t.Fatal(err)
}
if _, err := inv.theChain(ctx); !errors.Is(err, ErrNotLaidOut) {
t.Fatalf("a read before the layout answered %v", err)
if _, err := inv.theChain(ctx); err != nil {
t.Fatalf("the first read was refused, not laid out: %v", err)
}
if err := inv.LayBuildsOut(ctx, nil); err != nil {
t.Fatal(err)
if !containsKey(keysOf(t, mem), chain.BuildKey("alpha", old.ID)) {
t.Fatalf("the first read did not lay %s out under its module: %v", old.ID, keysOf(t, mem))
}
if _, err := inv.theChain(ctx); err != nil {
t.Fatalf("a read after the layout answered %v", err)
@@ -0,0 +1,25 @@
package inventory
import (
"context"
"testing"
"github.com/novox/mesh-controller/internal/chain"
)
// The first read of a chain that requires the layout lays the builds out itself when it may write, instead of refusing
// the controller's own start (issue 1121, 2026-10-18: the controller exited every five seconds).
func TestTheFirstReadOfAChainNotLaidOutLaysItOut(t *testing.T) {
ctx := context.Background()
inv, c := anInventoryOnTheChain(t)
inv.RequireLayout()
if _, found, _ := c.Builds.GetRaw(ctx, chain.LayoutMarkerKey); found {
t.Fatal("a fresh chain holds the marker already; the test proves nothing")
}
if _, err := inv.theChain(ctx); err != nil {
t.Fatalf("the first read was refused: %v", err)
}
if _, found, err := c.Builds.GetRaw(ctx, chain.LayoutMarkerKey); err != nil || !found {
t.Fatalf("the read did not lay the builds out: found %v, %v", found, err)
}
}