Files
mesh-catalog/modules/zfs/cmd/zfs-tools/bus.go
T
jschoubben 2d592c63c1 zfs: a keeper that does not answer is warned once, and RunKeeper closes its second connection (review round 2 finding 2, A-37)
A failed keeper call in a root process (hand acts, forget, Paused, every call) now adds one warning that the bus is
taken as down, instead of being dropped silently. RunKeeper closes the connection acct.Dial opens when the keeper
stops; the message id's comment says it is nanotime-pid.

Builds: design 42 zfs-owns-zfs A-37
2026-11-13 01:45:19 +01:00

281 lines
8.7 KiB
Go

package main
import (
"context"
"encoding/json"
"fmt"
"os"
"strings"
"sync"
"time"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/buscalls"
"git.novox.be/novox/mesh-sdk/go/process"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
)
// busStates is the module's states as this process reaches them (its bus account's `state`).
type busStates struct{ c *process.Conn }
func (b busStates) Put(state, key string, v any) error {
_, err := b.c.State(state).Put(key, v)
return err
}
func (b busStates) Delete(state, key string) error { return b.c.State(state).Delete(key) }
func (b busStates) Keys(state string) ([]string, error) { return b.c.State(state).Keys() }
func (b busStates) Get(state, key string) (json.RawMessage, bool, error) {
e, err := b.c.State(state).Get(key)
if err != nil {
return nil, false, err
}
if e == nil {
return nil, false, nil
}
return e.Value, true, nil
}
// JoinBus gives a root process the module's events and states through the keeper's socket (A-37): the process holds no
// bus account of its own. Without the socket variable the machine keeps none, and what it changes is said as a warning (§18:
// with the bus down the apply converges from the config file it last got). It answers how to leave the bus.
func (m *Machine) JoinBus() func() {
sock := strings.TrimSpace(os.Getenv(KeeperSocketEnv))
if sock == "" {
m.Warnings = append(m.Warnings, KeeperSocketEnv+" is not set, so the keeper is not reached and the bus is taken as down")
return func() {}
}
// Every call reaches the socket anew, so a keeper that comes up after this process is used from then on; one that
// is down at a call answers that call with an error, never the bus taken as down for the process's life.
// A call the keeper does not answer is said once, as a warning: the root process then acts as with the bus down (A-37).
var once sync.Once
c := keeperClient{sock: sock, down: func(err error) {
once.Do(func() {
m.Warnings = append(m.Warnings, "the bus is taken as down: "+err.Error())
})
}}
m.Emit = c.emit
m.States = c
m.Findings = meshconditionsclient.FindingWriter{State: findingState{c}}
m.Call = c.call
return func() {}
}
// RunKeeper is the process `zfs-keeper`: the module's one process bus account, serving the socket to root.
func RunKeeper(ctx context.Context) error {
sock := strings.TrimSpace(os.Getenv(KeeperSocketEnv))
if sock == "" {
return fmt.Errorf("%s is not set", KeeperSocketEnv)
}
acct, err := process.ReadBrokerAccount(os.Getenv)
if err != nil {
return err
}
c, err := process.Connect(acct)
if err != nil {
return err
}
defer c.Close()
l, err := Listen(sock)
if err != nil {
return err
}
nc, js, err := acct.Dial()
if err != nil {
return err
}
defer nc.Close()
k := &Keeper{Emit: func(kind string, body any) error { return publishEvent(js, acct, kind, body) }, States: busStates{c}, Call: c.Call}
return k.Serve(ctx, l)
}
// eventPublisher is the part of JetStream the keeper publishes with.
type eventPublisher interface {
PublishMsg(ctx context.Context, m *nats.Msg, opts ...jetstream.PublishOpt) (*jetstream.PubAck, error)
}
// publishEvent puts one event on the module's events stream as a JetStream publish the bus acknowledges, on the
// subject the stream captures, mesh.bus.<module>.events.<kind> (A-41); never process.Conn.Emit, whose subject the
// keeper's account is not granted. The message id (nanotime-pid) is the time in nanoseconds and the process id, so two events of one process never share one.
func publishEvent(js eventPublisher, acct process.Account, kind string, body any) error {
data, err := json.Marshal(body)
if err != nil {
return err
}
id := fmt.Sprintf("%d-%d", time.Now().UnixNano(), os.Getpid())
msg := nats.NewMsg(acct.BusSubject(EventsStream, kind))
msg.Header.Set("x-event-id", id)
msg.Header.Set("x-source", acct.Module)
msg.Header.Set("x-node", acct.Node)
msg.Header.Set("x-time", time.Now().UTC().Format(time.RFC3339Nano))
msg.Header.Set("content-type", "application/json")
msg.Data = data
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_, err = js.PublishMsg(ctx, msg, jetstream.WithMsgID(id))
return err
}
// toolStates is the module's states as the tools bundle reaches them, through the tool runner.
type toolStates struct{}
func (toolStates) Put(state, key string, v any) error {
_, err := stdio.State(state).Put(key, v)
return err
}
func (toolStates) Delete(state, key string) error { return stdio.State(state).Delete(key) }
func (toolStates) Keys(state string) ([]string, error) { return stdio.State(state).Keys() }
func (toolStates) Get(state, key string) (json.RawMessage, bool, error) {
e, err := stdio.State(state).Get(key)
if err != nil || e == nil {
return nil, false, err
}
return e.Value, true, nil
}
// eventHub hands the module's events, heard once through the tool runner, to the hand acts waiting for one.
type eventHub struct {
mu sync.Mutex
started bool
waiters map[int]hubWaiter
next int
}
type hubWaiter struct {
kinds []string
ch chan map[string]any
}
func (h *eventHub) await(kinds []string, bound time.Duration) (map[string]any, bool) {
h.mu.Lock()
if h.waiters == nil {
h.waiters = map[int]hubWaiter{}
}
h.next++
id, w := h.next, hubWaiter{kinds, make(chan map[string]any, 1)}
h.waiters[id] = w
first := !h.started
h.started = true
h.mu.Unlock()
defer func() { h.mu.Lock(); delete(h.waiters, id); h.mu.Unlock() }()
if first {
_ = stdio.Subscribe("#", func(e stdio.Envelope) error {
var body map[string]any
_ = json.Unmarshal(e.Body, &body)
h.mu.Lock()
defer h.mu.Unlock()
for _, w := range h.waiters {
for _, k := range w.kinds {
if e.Key == k || strings.HasSuffix(e.Key, "."+k) {
select {
case w.ch <- body:
default:
}
}
}
}
return nil
})
}
select {
case ev := <-w.ch:
return ev, true
case <-time.After(bound):
return nil, false
}
}
// busLedger is the ledger `snapshots` through the tool runner.
type busLedger struct{}
func (busLedger) Apply(instance, operation string, body any, call string) error {
raw, err := json.Marshal(body)
if err != nil {
return err
}
_, err = stdio.BusState(buscalls.LedgerApply, buscalls.ApplyRequest{Ledger: LedgerName, Instance: instance,
Operation: operation, Body: raw, Call: call})
return err
}
// takenOf reads a ledger get's answer: an instance never written answers state null and sequence 0, which is empty (A-40).
func takenOf(raw []byte) (struct {
Taken map[string]LedgerEntry `json:"taken"`
}, error) {
var body struct {
Taken map[string]LedgerEntry `json:"taken"`
}
var st buscalls.InstanceState
if err := json.Unmarshal(raw, &st); err != nil {
return body, err
}
if st.Sequence == 0 && (len(st.State) == 0 || string(st.State) == "null") {
return body, nil
}
err := json.Unmarshal(st.State, &body)
return body, err
}
func (busLedger) Entries(datasets []string) ([]LedgerEntry, error) {
var out []LedgerEntry
for _, ds := range datasets {
for _, kind := range keepKinds {
raw, err := stdio.BusState(buscalls.LedgerGet, buscalls.InstanceRequest{Ledger: LedgerName, Instance: LedgerInstance(kind, ds)})
if err != nil {
return nil, err
}
body, err := takenOf(raw)
if err != nil {
return nil, err
}
for _, name := range sortedKeys(body.Taken) {
out = append(out, body.Taken[name])
}
}
}
return out, nil
}
// EventsStream is the module's events stream's local name in its `requires` (§25).
const EventsStream = "events"
// busStream is the module's events stream through the tool runner.
type busStream struct{}
// Last reads the stream from its first sequence on, page by page, and keeps the last n events of the kind.
func (busStream) Last(n int, kind string) ([]LoggedEvent, error) {
raw, err := stdio.BusState(buscalls.StreamFill, buscalls.StreamRequest{Stream: EventsStream})
if err != nil {
return nil, err
}
var fill buscalls.StreamFilled
if err := json.Unmarshal(raw, &fill); err != nil {
return nil, err
}
var out []LoggedEvent
from := fill.First
for from != 0 && from <= fill.Last {
raw, err := stdio.BusState(buscalls.StreamRead, buscalls.StreamReadRequest{Stream: EventsStream, From: from, Limit: 500})
if err != nil {
return nil, err
}
var page buscalls.StreamPage
if err := json.Unmarshal(raw, &page); err != nil {
return nil, err
}
for _, msg := range page.Messages {
k := KindOfSubject(msg.Subject)
if kind != "" && k != kind {
continue
}
out = append(out, LoggedEvent{Sequence: msg.Sequence, Kind: k, At: msg.At, Body: msg.Body})
if len(out) > n {
out = out[1:]
}
}
from = page.Next
}
return out, nil
}