R-1: W1.3: a process reads its own build from MESH_FINGERPRINT once, and says it
The node-engine hands each process the fingerprint of the want it applies (design 47, The fingerprint handover; mesh-host W13.1). process.OwnBuild reads it once at the first ask, which Connect and Dial make before they dial, and says it in the journal; with none, or one that is no single word, it says "own build unknown" and why. Builds: design 47 G §201, §205, §239 Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
@@ -294,3 +294,50 @@ func TestModuleAccountNamesAndDialOptions(t *testing.T) {
|
||||
t.Errorf("a fingerprint did not pin the connection")
|
||||
}
|
||||
}
|
||||
|
||||
// W1.3 (design 47 *The fingerprint handover*): a process's own build is the fingerprint the node-engine handed it, read
|
||||
// once at its start; with none, or with one that is no single word, it says its own build is unknown and why.
|
||||
func TestTheOwnBuildIsTheFingerprintTheProcessWasStartedWith(t *testing.T) {
|
||||
for _, c := range []struct{ name, env, fp, unknown string }{
|
||||
{"handed", "src2:1480985e", "src2:1480985e", ""},
|
||||
{"handed with spaces around", " src2:1480985e\n", "src2:1480985e", ""},
|
||||
{"not handed", "", "", "own build unknown: started without MESH_FINGERPRINT"},
|
||||
{"not one word", "src2:1 src2:2", "", `own build unknown: MESH_FINGERPRINT "src2:1 src2:2" is not one fingerprint`},
|
||||
} {
|
||||
b := ReadOwnBuild(func(k string) string {
|
||||
if k != FingerprintEnv {
|
||||
t.Fatalf("%s: read %s", c.name, k)
|
||||
}
|
||||
return c.env
|
||||
})
|
||||
if b.Fingerprint != c.fp || b.Unknown != c.unknown {
|
||||
t.Errorf("%s: got %+v, want fingerprint %q, unknown %q", c.name, b, c.fp, c.unknown)
|
||||
}
|
||||
}
|
||||
var said []Build
|
||||
keptSay, keptOnce := sayOwnBuild, ownBuild
|
||||
t.Cleanup(func() { sayOwnBuild, ownBuild = keptSay, keptOnce })
|
||||
sayOwnBuild, ownBuild = func(b Build) { said = append(said, b) }, sync.OnceValue(readAndSay)
|
||||
t.Setenv(FingerprintEnv, "src2:aaaa")
|
||||
first := OwnBuild()
|
||||
t.Setenv(FingerprintEnv, "src2:bbbb")
|
||||
if again := OwnBuild(); again != first || first.Fingerprint != "src2:aaaa" {
|
||||
t.Errorf("read once: first %+v, then %+v", first, again)
|
||||
}
|
||||
if len(said) != 1 || said[0].String() != "src2:aaaa" {
|
||||
t.Errorf("the journal is told once, at the first read: %+v", said)
|
||||
}
|
||||
// Both ways a process reaches the bus read it at the start, before they dial: a refused dial still said it.
|
||||
for name, dial := range map[string]func(Account) error{
|
||||
"Connect": func(a Account) error { _, err := Connect(a); return err },
|
||||
"Dial": func(a Account) error { _, _, err := a.Dial(nats.RetryOnFailedConnect(false)); return err },
|
||||
} {
|
||||
said, ownBuild = nil, sync.OnceValue(readAndSay)
|
||||
if err := dial(Account{URL: "nats://127.0.0.1:1", User: "n1.m", Password: "pw"}); err == nil {
|
||||
t.Fatalf("%s: dialled a port nothing listens on", name)
|
||||
}
|
||||
if len(said) != 1 || said[0].Fingerprint != "src2:bbbb" {
|
||||
t.Errorf("%s: the process's own build is not said at its start: %+v", name, said)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ var refusedPublish = regexp.MustCompile(`(?i)Publish to "(\S+)"`)
|
||||
// permission violation refusing a publish this connection is waiting an answer to, and cancels that wait
|
||||
// at once.
|
||||
func Connect(account Account, opts ...nats.Option) (*Conn, error) {
|
||||
OwnBuild()
|
||||
c := &Conn{account: account, pending: map[string]map[uint64]context.CancelCauseFunc{}}
|
||||
nc, err := nats.Connect(account.URL, c.options(opts)...)
|
||||
if err != nil {
|
||||
|
||||
@@ -52,6 +52,7 @@ func (a Account) DialOptions(opts ...nats.Option) []nats.Option {
|
||||
// Dial connects as the module's own principal and opens JetStream on the connection. The caller closes the
|
||||
// connection.
|
||||
func (a Account) Dial(opts ...nats.Option) (*nats.Conn, jetstream.JetStream, error) {
|
||||
OwnBuild()
|
||||
nc, err := nats.Connect(a.URL, a.DialOptions(opts...)...)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("connecting to the bus: %w", err)
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package process
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// FingerprintEnv is the environment value the node-engine starts each process with, a service and a container alike:
|
||||
// the fingerprint of the want it applies (novox/hq ADR 0656; design 47 *The fingerprint handover*).
|
||||
const FingerprintEnv = "MESH_FINGERPRINT"
|
||||
|
||||
// OwnBuildUnknown is what a process started with no fingerprint says of its own build, in its journal and to whoever
|
||||
// reads it (the node-engine's process builds, the indexer acting on it as ADR 0639 §8 has it).
|
||||
const OwnBuildUnknown = "own build unknown"
|
||||
|
||||
// Build is the build a process was started with: its fingerprint, or why it is unknown.
|
||||
type Build struct {
|
||||
Fingerprint string `json:"fingerprint,omitempty"`
|
||||
// Unknown says why there is no fingerprint, starting with OwnBuildUnknown; empty when there is one.
|
||||
Unknown string `json:"unknown,omitempty"`
|
||||
}
|
||||
|
||||
// String is the fingerprint, or why the build is unknown.
|
||||
func (b Build) String() string {
|
||||
if b.Fingerprint != "" {
|
||||
return b.Fingerprint
|
||||
}
|
||||
return b.Unknown
|
||||
}
|
||||
|
||||
// ReadOwnBuild is the build getenv's FingerprintEnv names: one word, or unknown with the reason.
|
||||
func ReadOwnBuild(getenv func(string) string) Build {
|
||||
fp := strings.TrimSpace(getenv(FingerprintEnv))
|
||||
switch {
|
||||
case fp == "":
|
||||
return Build{Unknown: OwnBuildUnknown + ": started without " + FingerprintEnv}
|
||||
case strings.ContainsAny(fp, " \t\r\n"):
|
||||
return Build{Unknown: fmt.Sprintf("%s: %s %q is not one fingerprint", OwnBuildUnknown, FingerprintEnv, fp)}
|
||||
}
|
||||
return Build{Fingerprint: fp}
|
||||
}
|
||||
|
||||
// sayOwnBuild writes the process's own build in its journal; a variable so a test reads what it says.
|
||||
var sayOwnBuild = func(b Build) { log.Printf("own build: %s", b) }
|
||||
|
||||
// readAndSay reads the build from this process's environment and says it.
|
||||
func readAndSay() Build {
|
||||
b := ReadOwnBuild(os.Getenv)
|
||||
sayOwnBuild(b)
|
||||
return b
|
||||
}
|
||||
|
||||
// ownBuild is read once; a test that reads it makes it again (sync.OnceValue(readAndSay)) and puts it back.
|
||||
var ownBuild = sync.OnceValue(readAndSay)
|
||||
|
||||
// OwnBuild is this process's own build, read once from its environment at the first ask and said in its journal then;
|
||||
// every later ask answers the same, whatever the environment says since. Connect and Dial ask it, so every process
|
||||
// that reaches the bus says it at its start.
|
||||
func OwnBuild() Build { return ownBuild() }
|
||||
Reference in New Issue
Block a user