diff --git a/go/process/account_test.go b/go/process/account_test.go index 2d6e54e0..12fbc3b8 100644 --- a/go/process/account_test.go +++ b/go/process/account_test.go @@ -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) + } + } +} diff --git a/go/process/conn.go b/go/process/conn.go index ca469221..3809c429 100644 --- a/go/process/conn.go +++ b/go/process/conn.go @@ -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 { diff --git a/go/process/module.go b/go/process/module.go index de682222..f4b73678 100644 --- a/go/process/module.go +++ b/go/process/module.go @@ -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) diff --git a/go/process/ownbuild.go b/go/process/ownbuild.go new file mode 100644 index 00000000..c9a18ad4 --- /dev/null +++ b/go/process/ownbuild.go @@ -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() }