Files
mesh-controller/internal/inventory/chain.go
T
jschoubben a825a4c904
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 first read of a chain not laid out lays it out, so no read of the start is refused (issue 1121)
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).
2026-10-18 14:39:43 +02:00

132 lines
4.7 KiB
Go

package inventory
import (
"context"
"errors"
"fmt"
"sync"
"github.com/novox/mesh-controller/internal/chain"
)
// **The chain lives on the bus** (novox/hq ADR 0408, design 47 milestone 5): builds, rollouts and assignments are keys of the
// bus's key-value store, the controller's store keeps none of them, and every method of a kind reads and writes the chain this
// inventory is given (theChain).
// testChains are the chains in memory a test made for its store, by the store's address: every inventory opened on that
// store is given the same chain, as every process of a mesh is on the same bus. Empty outside a test.
var testChains sync.Map
// ErrNoChain is a read or write of the chain by an inventory that has none attached: a process that did not open the bus.
// Refused loudly.
var ErrNoChain = errors.New("this kind of record lives on the bus, and this process has not opened the bus")
// chainState is what an inventory keeps of the chain it is given.
type chainState struct {
mu sync.Mutex
open ChainOpener
// view is the rollouts bucket kept in memory by one watch (issue 1092); pending are the merges added to a plan that
// is not saved yet.
view *rolloutView
pending map[string]pendingMerge
// requireLayout is set for a chain on the bus: its builds are read only once the controller's start has laid them out
// under their modules (issue 1121), and laid is that seen.
requireLayout, laid bool
}
// RequireLayout makes every read of the chain refuse (ErrNotLaidOut) until the builds are laid out under their modules.
func (i *Inventory) RequireLayout() {
i.state.mu.Lock()
i.state.requireLayout = true
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
i.state.mu.Unlock()
}
// ChainOpener opens the chain on the bus when a process first needs it: a command that reads and never moved anything has
// no bus connection until a kind it reads turns out to be moved.
type ChainOpener func(ctx context.Context) (*chain.Chain, error)
// UseChainOpener gives the inventory the means to open the chain when it needs it and none is attached.
func (i *Inventory) UseChainOpener(open ChainOpener) {
i.state.mu.Lock()
i.state.open = open
i.state.requireLayout = true
i.state.mu.Unlock()
}
// UseChain attaches the chain the moved kinds are read and written through. The serving controller attaches the bus's,
// holding the lease for its writes; a test attaches one in memory.
func (i *Inventory) UseChain(c *chain.Chain) {
i.state.mu.Lock()
defer i.state.mu.Unlock()
i.chain = c
if i.state.view != nil {
i.state.view.stop()
i.state.view = nil
}
}
// UseThrowawayChain gives the inventory a chain in memory that is its own, laid out already: for a mesh raised for a moment
// beside the real one, as the merge gate raises the facts snapshot's mesh in a throwaway store (issue 1121). Nothing of it
// reaches the bus.
func (i *Inventory) UseThrowawayChain() {
i.UseChain(chain.InMemory(nil))
i.markLaidOut()
}
// attached is the chain this inventory has, nil when none.
func (i *Inventory) attached() *chain.Chain {
i.state.mu.Lock()
defer i.state.mu.Unlock()
return i.chain
}
// theChain is the chain this inventory writes and reads, opened if a command has not yet. On the bus it is refused until the
// builds are laid out under their modules.
func (i *Inventory) theChain(ctx context.Context) (*chain.Chain, error) {
i.state.mu.Lock()
if i.chain == nil && i.state.open != nil {
c, err := i.state.open(ctx)
if err != nil {
i.state.mu.Unlock()
return nil, fmt.Errorf("%w: %v", ErrNoChain, err)
}
i.chain = c
}
c, check := i.chain, i.state.requireLayout && !i.state.laid
i.state.mu.Unlock()
if c == nil {
return nil, ErrNoChain
}
if check {
if _, found, err := c.Builds.GetRaw(ctx, chain.LayoutMarkerKey); err != nil {
return nil, err
} else if !found {
// **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()
}
return c, nil
}