Serve the read verbs on the serving controller's own connection, and name every connection

Each verb ran as a process that dialled the bus, so hundreds of short
connections an hour, all named mesh-controller, hid any client reconnecting
in a loop (hq issue 327). D15 now says a user whose connections keep dropping.
This commit is contained in:
jochen
2026-10-08 21:08:34 +02:00
parent ca09a07fdf
commit 1e04670052
21 changed files with 744 additions and 80 deletions
+21 -2
View File
@@ -28,6 +28,8 @@ import (
type JetStream struct {
conn *nats.Conn
js nats.JetStreamContext
// borrowed is a connection lent by its owner (Borrow): closing it is the owner's.
borrowed bool
// Note is how this says something it decided not to fail over. Nil is silent, which is only
// right for a caller that has no way to report; the controller sets it.
Note func(string, ...any)
@@ -40,9 +42,17 @@ func (j *JetStream) note(format string, args ...any) {
}
}
// ConnectionName is what a connection this process dials says it is, in the server's list of
// connections: the controller's role and what it is doing (novox/hq issue 327). Every connection was
// named `mesh-controller` — the serving controller's, each verb's own process, the build agents' — so the
// server's list could not say which was which. The process sets it once, at its start; an option a
// caller passes to Dial names one connection otherwise.
var ConnectionName = "mesh-controller"
// Dial connects and returns the controller's JetStream handle.
func Dial(url string, opts ...nats.Option) (*JetStream, error) {
opts = append(opts, nats.Name("mesh-controller"), nats.Timeout(10*time.Second))
// The name first, so a name the caller gives is the one that stands.
opts = append([]nats.Option{nats.Name(ConnectionName), nats.Timeout(10 * time.Second)}, opts...)
// **Pinned, not named.** The bus presents the mesh's own certificate, which names nothing a
// public verifier would accept (design 25 §4: a host pins the server's exact certificate and
// checks nothing else, and so does this). Without this, the first connection failed with
@@ -130,11 +140,20 @@ func (j *JetStream) Conn() *nats.Conn { return j.conn }
func (j *JetStream) Context() nats.JetStreamContext { return j.js }
func (j *JetStream) Close() {
if j.conn != nil {
if j.conn != nil && !j.borrowed {
j.conn.Close()
}
}
// Borrow is the same connection for a caller that will close what it was handed when it is done: its
// Close closes nothing, and the connection stays its owner's (novox/hq issue 327). How the serving
// controller lends its own connection to work that would otherwise dial one of its own.
func Borrow(j *JetStream) *JetStream {
lent := *j
lent.borrowed = true
return &lent
}
// EnsureStream creates the stream if it is absent and brings it to match if it is present.
//
// **Idempotent, because the controller asserts on every start** rather than creating once at
+31
View File
@@ -3,6 +3,8 @@ package broker
import (
"testing"
"github.com/nats-io/nats.go"
"github.com/novox/mesh-controller/internal/testbus"
)
@@ -66,3 +68,32 @@ func TestAgainstARealServer(t *testing.T) {
}
})
}
// A connection says what it is in the server's list (novox/hq issue 327): the process's name, unless the
// caller names this one; and a lent connection's Close leaves its owner's open.
func TestAConnectionIsNamedAndALentOneIsNotClosed(t *testing.T) {
url := testbus.URL(t)
was := ConnectionName
ConnectionName = "mesh-controller verb conditions"
defer func() { ConnectionName = was }()
named, err := Dial(url)
if err != nil {
t.Fatal(err)
}
defer named.Close()
if got := named.Conn().Opts.Name; got != "mesh-controller verb conditions" {
t.Errorf("named %q", got)
}
lease, err := Dial(url, nats.Name("mesh-controller serving lease"))
if err != nil {
t.Fatal(err)
}
defer lease.Close()
if got := lease.Conn().Opts.Name; got != "mesh-controller serving lease" {
t.Errorf("a name the caller gave became %q", got)
}
Borrow(named).Close()
if !named.Conn().IsConnected() {
t.Error("closing a lent connection closed its owner's")
}
}
+6
View File
@@ -47,6 +47,12 @@ func BuildsOverNATSOn(address, seat string) (Builders, error) {
return &natsBuilds{js: js, owned: true, seat: seat}, nil
}
// BuildsOn is the asking side on a connection the caller holds, which Close leaves open: the serving
// controller asks on its own rather than dialling one per build (novox/hq issue 327).
func BuildsOn(js *broker.JetStream, seat string) Builders {
return &natsBuilds{js: js, seat: seat}
}
// role is the seat asked: what the asker was made for, or the current build role for one made
// without saying (a test building the struct by hand).
func (b *natsBuilds) role() string {