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() } 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 { return nil, ErrNotLaidOut } i.markLaidOut() } return c, nil }