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:.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 } // 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) }