Main reverted G's half merge (#189, reverted by8afc3ebin #191) and then took the reader release's S1-S5, which copied G's code. G's form holds where they differ (process_bus.go, seats.go, state.go and their tests); the release's kept names stay so main's callers keep building: SeatViewPrefix beside SeatReadPrefix, and QueueInvoke's empty strings on not-ok (with its test TestQueueInvoke). The tree differs from G's headf30dee54only by those and the release's two tests (objects cap option, seat states and stream kinds). Builds: design 47 G (the milestone's sdk part, as atf30dee54) Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
192 lines
6.2 KiB
Go
192 lines
6.2 KiB
Go
package process
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/nats-io/nats.go/jetstream"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/membership"
|
|
"git.novox.be/novox/mesh-sdk/go/seatdef"
|
|
)
|
|
|
|
// Binding is how a process with an account of its own reaches the kept data its `requires` declares: Names takes a local
|
|
// name and gives the entry's bus names (bucket, stream and subjects, object stores). It asks `show` of the seat
|
|
// the bus's seat for the module and each local name at its start, as a kept call, and asks again when it hears the seat's
|
|
// `recreated` or `dropped` for that entry (Register). Until `show` answers, Names answers an error naming the entry and
|
|
// the process acts on nothing that needs it; Waiting says so for the part's health (design 47 G §577; ADR 0283).
|
|
type Binding struct {
|
|
seat string
|
|
module string
|
|
call func(key string, body any) (json.RawMessage, error)
|
|
logf func(string, ...any)
|
|
|
|
mu sync.Mutex
|
|
entries map[string]*boundEntry
|
|
locals []string
|
|
// spawn runs an ask of its own; a test makes it synchronous.
|
|
spawn func(func())
|
|
}
|
|
|
|
type boundEntry struct {
|
|
names membership.BusBinding
|
|
have bool
|
|
why string // why there is no answer yet
|
|
ask int // the latest ask; an older answer is dropped
|
|
}
|
|
|
|
// NewBinding is a Binding of module's entries, asked of the seat that holds the bus's state (the caller names it, as it
|
|
// names the seat in its `invokes` as `seat:<seat>.show`: this package names no seat), by their local names, asking through call (a Conn's CallKept); logf says
|
|
// what it waits for (nil says nothing).
|
|
func NewBinding(seat, module string, call func(key string, body any) (json.RawMessage, error), logf func(string, ...any), locals ...string) *Binding {
|
|
if logf == nil {
|
|
logf = func(string, ...any) {}
|
|
}
|
|
b := &Binding{seat: seat, module: module, call: call, logf: logf, entries: map[string]*boundEntry{}, spawn: func(f func()) { go f() }}
|
|
for _, l := range locals {
|
|
if _, ok := b.entries[l]; !ok {
|
|
b.entries[l] = &boundEntry{why: "not asked yet"}
|
|
b.locals = append(b.locals, l)
|
|
}
|
|
}
|
|
return b
|
|
}
|
|
|
|
// Names are the bus names of the entry local, or an error naming the entry when `show` has not answered for it (or it is
|
|
// no entry this Binding was made for).
|
|
func (b *Binding) Names(local string) (membership.BusBinding, error) {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
e := b.entries[local]
|
|
switch {
|
|
case e == nil:
|
|
return membership.BusBinding{}, fmt.Errorf("%s asks no bus entry %q: its Binding holds %s", b.module, local, strings.Join(b.locals, ", "))
|
|
case !e.have:
|
|
return membership.BusBinding{}, fmt.Errorf("the bus entry %q of %s is not bound yet: %s", local, b.module, e.why)
|
|
}
|
|
return e.names, nil
|
|
}
|
|
|
|
// Asks says whether local is an entry this Binding was made for.
|
|
func (b *Binding) Asks(local string) bool {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
return b.entries[local] != nil
|
|
}
|
|
|
|
// Waiting is why the process waits, naming each entry `show` has not answered for, or empty when every one is bound.
|
|
func (b *Binding) Waiting() string {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
var out []string
|
|
for _, l := range b.locals {
|
|
if e := b.entries[l]; !e.have {
|
|
out = append(out, fmt.Sprintf("the bus entry %q (%s)", l, e.why))
|
|
}
|
|
}
|
|
if len(out) == 0 {
|
|
return ""
|
|
}
|
|
return "waits for " + strings.Join(out, ", ")
|
|
}
|
|
|
|
// Start asks `show` for every entry once, each as a kept call of its own that returns when answered, so the process's
|
|
// start waits for none.
|
|
func (b *Binding) Start() {
|
|
for _, l := range b.locals {
|
|
l := l
|
|
b.spawn(func() { b.Ask(l) })
|
|
}
|
|
}
|
|
|
|
// Ask asks `show` for the entry local once more, as a kept call, and keeps its answer; the answer of an ask replaced by a
|
|
// later one is dropped.
|
|
func (b *Binding) Ask(local string) {
|
|
b.mu.Lock()
|
|
e := b.entries[local]
|
|
if e == nil {
|
|
b.mu.Unlock()
|
|
return
|
|
}
|
|
e.ask++
|
|
mine := e.ask
|
|
b.mu.Unlock()
|
|
names, err := b.show(local)
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
if e.ask != mine {
|
|
return
|
|
}
|
|
if err != nil {
|
|
e.have, e.why = false, err.Error()
|
|
b.logf("binding: the bus entry %q of %s is not bound: %v", local, b.module, err)
|
|
return
|
|
}
|
|
e.names, e.have, e.why = names, true, ""
|
|
}
|
|
|
|
func (b *Binding) show(local string) (membership.BusBinding, error) {
|
|
raw, err := b.call("seat:"+b.seat+".show", map[string]string{"module": b.module, "local-name": local})
|
|
if err != nil {
|
|
return membership.BusBinding{}, err
|
|
}
|
|
var held struct {
|
|
Binding *membership.BusBinding `json:"binding"`
|
|
State string `json:"state"`
|
|
Why string `json:"why"`
|
|
}
|
|
if err := json.Unmarshal(raw, &held); err != nil {
|
|
return membership.BusBinding{}, fmt.Errorf("show answered something that is not an entry: %w", err)
|
|
}
|
|
if held.Binding == nil {
|
|
return membership.BusBinding{}, fmt.Errorf("the bus holds no objects for it yet (%s %s)", held.State, held.Why)
|
|
}
|
|
return *held.Binding, nil
|
|
}
|
|
|
|
// Register makes the Binding hear `recreated` and `dropped` of the seat on hub, and asks every entry again at each bind
|
|
// of the hub's consumer, an event said before it was made being undelivered (ADR 0668). Register before the hub starts.
|
|
func (b *Binding) Register(hub *EventHub) error {
|
|
for _, event := range []string{"recreated", "dropped"} {
|
|
if err := hub.Handle(seatdef.EventSubject(b.seat, event), b.Hear); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
hub.OnBound(func(time.Time) {
|
|
for _, l := range b.locals {
|
|
l := l
|
|
b.spawn(func() { b.Ask(l) })
|
|
}
|
|
})
|
|
return nil
|
|
}
|
|
|
|
// Hear takes a `recreated` or `dropped` of the seat: one for an entry of this module's Binding is asked again, and a
|
|
// dropped one is unbound until `show` answers. Any other is acknowledged and left.
|
|
func (b *Binding) Hear(m jetstream.Msg) {
|
|
defer func() { _ = m.Ack() }()
|
|
var said struct {
|
|
Module string `json:"module"`
|
|
Local string `json:"local-name"`
|
|
}
|
|
if json.Unmarshal(m.Data(), &said) != nil || said.Module != b.module {
|
|
return
|
|
}
|
|
b.mu.Lock()
|
|
e := b.entries[said.Local]
|
|
if e != nil && strings.HasSuffix(m.Subject(), ".dropped") {
|
|
e.have, e.why = false, "the bus dropped it"
|
|
}
|
|
b.mu.Unlock()
|
|
if e != nil {
|
|
b.spawn(func() { b.Ask(said.Local) })
|
|
}
|
|
}
|
|
|
|
// Locals are the local names this Binding holds, in the order made.
|
|
func (b *Binding) Locals() []string { return slices.Clone(b.locals) }
|