Registration and Discovery
Registration is service discovery without a discovery daemon. When a managed resource reaches
a stable state, it publishes an entry into a NATS JetStream stream. Other nodes read those
entries, usually from a template, to build configuration such as a load-balancer upstream or a
Prometheus target list. Publishing is a side effect of apply; lookup is a template function
or a CLI query.
Where it lives
registration: the transport, subject grammar, stream, lookup, and watch. model/registration.go
and model/registration_ttl.go: the entry and TTL data model. Key files:
registration/subject.go, registration/stream.go, registration/nats.go,
registration/lookup.go, registration/watch.go.
The publish gate
A resource carries a register_when_stable list. After the resource applies,
publishRegistration (resources/apply/apply.go:736) decides whether to announce it. The gate
is conservative: it publishes only when the resource has entries, the manager is not in noop
mode, the event did not fail, and every health check returned OK. A resource with no health
checks passes that last test.
Load-bearing decision
A missing registration publisher is tolerated, not fatal. The manager defaults to a no-op
publisher, so a register_when_stable manifest runs safely on a node with no registration
backend. This is what lets the same manifest apply on nodes with and without NATS.
A KV store built from a stream
Each entry maps to a single subject. The address contributes its IP with dots turned into
underscores so it is one token, and an InstanceId FNV hash of the full tuple forms the final
token, so every unique cluster, service, protocol, address, and port maps to one stable
subject. The stream captures the whole namespace with MaxMsgsPerSubject: 1 and AllowRollup,
and reliable publishes carry a Nats-Rollup: sub header.
Load-bearing decision
One message per subject plus rollup plus a hashed per-instance subject makes the stream behave
like a KV store keyed on the entry tuple. Re-registering overwrites in place, and a read is
simply “last message per subject.” Full KV semantics only hold with the JetStream destination;
the plain nats destination is best-effort and omits the TTL and rollup headers.
Expiry, lookup, and watch
Expiry is two-tiered. A per-entry Nats-TTL header sits under the stream-wide MaxAge, and on
removal the server writes a subject delete marker so watchers see the removal rather than a
silent disappearance. The stream denies hard deletes but allows purge, so JetStreamRemove can
retire a single entry.
- Lookup
JetStreamLookupbuilds a wildcard filter subject, fetches the last message per matching subject, drops server marker messages, and returns entries sorted by address then port.- Watch
JetStreamWatchstreamsWatchEvents: normal messages becomeRegisterevents, marker messages becomeRemoveevents with the entry reconstructed from the subject.- From a template
- The manager exposes lookup as
registrations(cluster, protocol, service, ip), so a rendered config file discovers its peers directly. - Prometheus
RegistrationEntries.PrometheusFileSD()converts entries into file_sd JSON, grouped by cluster, service, and protocol.
Load-bearing decision
Both lookup and watch special-case the Nats-Marker-Reason header, per ADR-43. Lookup drops
markers; watch turns them into removals. A reader that ignored markers would surface expired or
purged entries as live services.
The Priority and Annotations fields travel on every entry but, within this subsystem, are
only consumed by the Prometheus conversion. Priority is validated and sorted around but not
otherwise acted on here, reserved for downstream consumers such as SRV-style weighting.
Next
Continue to Observability to see how each apply is recorded, measured, and health-checked.