Documentation
¶
Overview ¶
Package natslink packages "one NATS connection bound to one topic" as a self-supervising unit that works out of the box.
The goal is to let many programs share one battle-tested set of connection supervision logic:
- Probe / MustProbe: fail-fast probing at startup. Environment errors (address / network / token) surface within about 5s, on the first line of the process's life; a URL only needs to be probed once.
- Subscriber: one connection subscribing to one topic. Built-in connection supervision, automatic reconnect on disconnect, self-healing rebuild after a "permanent close" (with automatic re-subscription), and status / data statistics accumulated across rebuilds. Callbacks are dispatched in one of three modes: a bounded worker pool (default; back-pressure drops are visible), synchronous (SyncMode), or go-per-message (GoPerMessage, unbounded concurrency); see SubscriberOptions.Workers.
- Publisher: one connection publishing to one topic. The same supervision and self-healing rebuild; a failed publish returns an error and the caller decides whether to fail over or count a drop.
- PublisherPool: N Publishers with round-robin plus health-first failover (latency first: disconnected members are demoted to last-resort candidates, so a message always leaves on a healthy connection first).
- Mirrored constructors NewMirroredPublisherPool / NewMirroredSubscriber: mirror the same topic onto M mutually independent NATS endpoints (publish once per endpoint, subscribe once per endpoint; deduplication happens on the consumer side). They return the ordinary types and are used exactly the same way; intended for active-active topologies such as "two local leaf nodes, each with its own upstream". See mirror.go and the README section on mirroring.
The standard startup sequence is MustProbe(url) (environment fail-fast) -> New* (configuration fail-fast) -> MustStart. After that, every runtime disconnect or permanent close is handled by the package's self-healing logic: it never gives up and never fails silently (see the README's mental-model and failure-handling sections).
The package depends only on nats.go and the standard library; it carries no application code.
Instances are single-use: after Stop they cannot be reused; create a new one.
Index ¶
- Constants
- Variables
- func MustProbe(natsURL string)
- func MustProbeEach(natsURLs ...string)
- func Probe(natsURL string) error
- type DedupSet
- type Handler
- type Logger
- type Options
- type Publisher
- func (l *Publisher) Conn() *nats.Conn
- func (l *Publisher) IsConnected() bool
- func (p *Publisher) MustStart()
- func (p *Publisher) Publish(data []byte) error
- func (p *Publisher) Start() error
- func (l *Publisher) Stats() Stats
- func (p *Publisher) Stop()
- func (l *Publisher) Topic() string
- func (l *Publisher) WaitConnected(timeout time.Duration) error
- type PublisherOptions
- type PublisherPool
- func (p *PublisherPool) ConnectedCount() int
- func (p *PublisherPool) Dropped() uint64
- func (p *PublisherPool) Endpoints() int
- func (p *PublisherPool) MustStart()
- func (p *PublisherPool) Publish(data []byte) error
- func (p *PublisherPool) Size() int
- func (p *PublisherPool) Start() error
- func (p *PublisherPool) Stats() []Stats
- func (p *PublisherPool) Stop()
- func (p *PublisherPool) WaitConnected(timeout time.Duration) error
- type Stats
- type Subscriber
- func (s *Subscriber) Conn() *nats.Conn
- func (s *Subscriber) Endpoints() int
- func (s *Subscriber) IsConnected() bool
- func (s *Subscriber) IsPaused() bool
- func (s *Subscriber) MustStart()
- func (s *Subscriber) Pause() error
- func (s *Subscriber) Resume() error
- func (s *Subscriber) Start() error
- func (s *Subscriber) Stats() Stats
- func (s *Subscriber) Stop()
- func (s *Subscriber) Topic() string
- func (s *Subscriber) WaitConnected(timeout time.Duration) error
- type SubscriberOptions
Constants ¶
const GoPerMessage = -2
GoPerMessage, assigned to SubscriberOptions.Workers, selects go-per-message delivery (see that field).
const SyncMode = -1
SyncMode, assigned to SubscriberOptions.Workers, selects synchronous delivery (see that field).
const Version = "1.8.3"
Version is the package version. It mirrors the git tag (vX.Y.Z) and is logged at connection start, so operators can see which version each process runs.
Variables ¶
var ( ErrAlreadyStarted = errors.New("natslink: already started") ErrStopped = errors.New("natslink: already stopped; create a new instance") ErrNotRunning = errors.New("natslink: not running") ErrNoConn = errors.New("natslink: no usable connection (rebuilding)") ErrOversized = errors.New("natslink: payload exceeds MaxPayload") // ErrQueueFull is returned by a mirrored PublisherPool when no endpoint's // send queue could take the message (every endpoint is stalled or // backlogged); it is counted in the pool-level Dropped. ErrQueueFull = errors.New("natslink: every endpoint send queue is full") )
Functions ¶
func MustProbe ¶
func MustProbe(natsURL string)
MustProbe is like Probe but panics on failure. Intended for process startup.
func MustProbeEach ¶
func MustProbeEach(natsURLs ...string)
MustProbeEach calls MustProbe on every URL. It is the standard startup fail-fast for the mirrored constructors (multiple endpoints): any endpoint with an environment error panics immediately. Configuring M endpoints means making M promises. When an element is a comma-separated list (ordered failover within the endpoint), the servers are tried in the written order and reaching any one of them counts as success (matching that endpoint's runtime semantics); note that the worst case is then 5s per server, accumulated serially, so the "surfaces within 5s" promise is per server.
func Probe ¶
Probe opens a one-off connection to verify that natsURL is reachable and that authentication succeeds, then closes it immediately.
Why it exists: Start / MustStart enable RetryOnFailedConnect, so an unreachable server or a bad token is not a Start error (the connection is created anyway and retries forever in the background). That is exactly the self-healing behaviour wanted at runtime, but it silently swallows environment errors (typo in the address, firewall, expired token, services deployed in the wrong order) until someone reads the logs. The right semantics at startup are fail-fast: configuring NATS is a promise, and not being able to connect at startup is an environment error that should kill the process so the deployer notices right away.
Standard startup sequence (when several connections share one URL, probe it once; environment errors surface within about 5s instead of being stretched by N connections each waiting on their own):
natslink.MustProbe(url) // startup: environment error panics immediately, fail-fast pool.MustStart() // from here on an error can only be a configuration error (also fail-fast) sub.MustStart() // runtime disconnects are handled by the package's self-healing; it never gives up
Credentials in the URL are redacted in the returned error, so it is safe to log.
Types ¶
type DedupSet ¶ added in v1.7.0
type DedupSet struct {
// contains filtered or unexported fields
}
DedupSet remembers the hashes of the last N distinct payloads and reports whether a payload is a byte-identical copy of one of them. It is a general building block: give it to any Subscriber (SubscriberOptions.Dedup), share one across several or all subscribers of a process so copies arriving on different subjects collapse too, or call Seen directly on any path. The typical use is collapsing the M copies a mirrored subscription receives (one per endpoint) into a single callback without decoding anything.
It is a FIFO set of 64-bit maphash values: a ring buffer fixes the eviction order and a presized map does the lookup, so after warm-up a call allocates nothing. The hash is computed outside the lock; the critical section is one map lookup and, on a miss, one insert. A hit does not refresh the entry, so a flood of one message cannot pin it. With an optional time window an entry older than the window no longer counts as seen (useful on quiet subjects where a legitimately re-sent identical message would otherwise be dropped minutes later). A 64-bit collision inside the window would drop a distinct message; with 4096 entries that is about 2^-52 per message.
This collapses byte-identical copies only. Messages that carry the same content in different bytes (a different sender field, a timestamp) are not duplicates to this set; that deduplication belongs to the application.
func NewDedupSet ¶ added in v1.7.0
NewDedupSet creates a set remembering the last entries distinct payloads (<= 0 selects 4096). A positive window additionally expires an entry once it is older than the window; 0 disables age expiry. Memory is fixed at about 40 bytes per entry.
func (*DedupSet) Entries ¶ added in v1.7.0
Entries returns the number of distinct payloads the set remembers.
func (*DedupSet) Seen ¶ added in v1.7.0
Seen reports whether data is byte-identical to a payload recorded within the set's capacity (and window); if not, it records data and returns false. Safe for concurrent use.
type Handler ¶
Handler is the subscription message callback.
Concurrency and ordering contract: in worker-pool mode (the default) the callback is invoked concurrently by up to Workers goroutines, and processing order is not guaranteed to match publish order (several workers pull from the same queue), so shared state inside the callback needs its own synchronisation. In go-per-message mode the callback runs on detached goroutines with no upper bound on concurrency, and again without ordering. For strict ordering use Workers: 1 (ordered, delivery never blocks) or Workers: SyncMode (ordered, but blocks the NATS delivery goroutine). Under a mirrored subscription (NewMirroredSubscriber) the callback receives every copy from every endpoint; the ordering guarantees above hold only within a single endpoint, and copies from different endpoints interleave freely. Mirrored x SyncMode warning: the "serial and ordered" property of synchronous mode does not survive mirroring. Each of the M endpoints has its own NATS delivery goroutine, so the callback is invoked concurrently by M goroutines. A callback that relies on SyncMode serialisation to skip locking is a data race under mirroring and must be made concurrency-safe.
In worker-pool and go-per-message modes the callback may do slow work. In synchronous mode it runs on the NATS delivery goroutine, and blocking for too long triggers slow-consumer drops (counted in SlowConsumers).
msg.Data is memory allocated for that message alone (nats.go does not reuse its read buffer), so the callback may retain it after returning (store it, hand it to another goroutine) without a defensive copy.
type Logger ¶
type Logger interface {
Info(msg string, ctx ...any)
Warn(msg string, ctx ...any)
Error(msg string, ctx ...any)
}
Logger is the minimal logging interface the package needs. The standard library's *slog.Logger and go-ethereum's log.Logger both satisfy it as-is: pass slog.Default() or log.Root() directly.
type Options ¶
type Options struct {
URL string // Required. Full NATS URL, e.g. nats://token@host:4222 (typically the nearest server or leaf node).
Topic string // Required. The single topic this connection is bound to.
Name string // Client name (shown in server monitoring and logs); defaults to natslink-<role>-<topic>.
Logger Logger // Log output; nil means slog.Default().
// ReportInterval is the interval of the built-in status report: every
// interval one Info line with statistics is logged, plus a Warn line when
// not connected. Defaults to 1 minute; a negative value disables it, in
// which case the caller aggregates Stats() itself.
//
// The supervision parameters (reconnect backoff, heartbeat, rebuild
// backoff, ...) are package constants, see the top of natslink.go; they
// need no configuration.
ReportInterval time.Duration
// ExtraNatsOptions are additional options appended to nats.Connect (e.g.
// TLS configuration, a custom dialer). They are the escape hatch for
// overriding the underlying policy parameters. Two caveats:
// - overriding MaxReconnects / RetryOnFailedConnect / CustomReconnectDelay /
// PingInterval / MaxPingsOutstanding removes the corresponding line of
// defence from the three-layer failure model; you are on your own;
// - the event callbacks (ClosedHandler / ErrorHandler / DisconnectErrHandler /
// ...) are injected after these options and cannot be overridden: the
// self-healing rebuild and the SlowConsumers observation are the core
// promise of this package and there is no way to remove them.
ExtraNatsOptions []nats.Option
}
Options is the connection configuration shared by Subscriber and Publisher. Zero-valued fields use defaults.
type Publisher ¶
type Publisher struct {
// contains filtered or unexported fields
}
Publisher is a self-supervising connection dedicated to publishing on a single topic. Usage: NewPublisher -> Start -> Publish(...) -> Stop.
While disconnected, Publish writes into the reconnect buffer (ReconnectBufSize) and returns nil; the buffered messages are flushed automatically once reconnected. It returns an error only when the buffer overflows or the connection is inside a self-healing rebuild, leaving failover or discard to the caller. For round-robin over several connections with failover use PublisherPool.
func NewPublisher ¶
func NewPublisher(o PublisherOptions) (*Publisher, error)
NewPublisher creates a publishing connection (not yet connected; call Start).
func (*Publisher) Conn ¶
Conn returns the current underlying connection; it may be nil inside a rebuild window. It is an escape hatch only (e.g. for Request-Reply); subscribing or publishing through it directly is outside this package's supervision.
func (*Publisher) IsConnected ¶
func (l *Publisher) IsConnected() bool
IsConnected reports whether the connection is currently established (false while RECONNECTING or inside a rebuild window).
func (*Publisher) MustStart ¶
func (p *Publisher) MustStart()
MustStart is Start but panics on failure.
Note: MustStart succeeding does not mean connected (it succeeds even when the server is unreachable; retries continue in the background). For fail-fast "die if it cannot connect" semantics at startup, call MustProbe first (the standard pattern is in the Probe documentation).
func (*Publisher) Publish ¶
Publish sends one message on the bound topic. A nil return means the connection accepted the message: sent immediately when connected, or written into the reconnect buffer during RECONNECTING and flushed on reconnect (core NATS has no delivery guarantee, see README). A non-nil error means the message was not accepted. Possible errors (all but the first two pre-check rejections are counted in Stats.Dropped):
ErrStopped the instance has been stopped (single use; create a new one) ErrNotRunning Start has not been called ErrOversized larger than MaxPayload ErrNoConn the connection is inside a rebuild window (old connection permanently closed, new one not yet swapped in) other an underlying nats error (e.g. reconnect buffer overflow, nats.ErrReconnectBufExceeded)
Safe for concurrent use. data is copied synchronously into the send/reconnect buffer before return, so the caller may reuse it immediately.
func (*Publisher) Start ¶
Start opens the connection. A temporarily unreachable server is not an error (it retries in the background, and Publish writes into the reconnect buffer meanwhile); the returned error is essentially only a configuration error such as a bad URL and should be treated as fatal.
func (*Publisher) Stats ¶
func (l *Publisher) Stats() Stats
Stats returns a snapshot of the current status and data statistics. It is safe to call concurrently at any time.
func (*Publisher) Stop ¶
func (p *Publisher) Stop()
Stop makes a best-effort drain of the pending send buffer (1 second timeout) and then closes the connection. While disconnected the drain necessarily fails and the unsent messages in the reconnect buffer are discarded (a Warn log carries the buffered byte count). Bounded promise: the whole call blocks at most stopGrace (5s); if the peer is wedged the wait is abandoned and the drain and close continue in the background (see link.stop). In that case Stop returning does not mean the connection is closed, and the Warn for a failed flush may come after the return (lost if the process exits right away). Stop is idempotent and safe to call repeatedly; the instance is not reusable afterwards.
func (*Publisher) Topic ¶
func (l *Publisher) Topic() string
Topic returns the topic this connection is bound to.
func (*Publisher) WaitConnected ¶
WaitConnected blocks until the connection is ready, or returns an error on timeout (including the current status and the underlying error, e.g. an authentication failure).
This is a "wait until ready" utility (for tests, or when the application must be online before its first message). For startup fail-fast use MustProbe (see the Probe documentation): one probe connection exposes environment errors without every connection waiting out its own timeout. Also note that on a bad token this method waits the full timeout before returning (auth errors are deliberately treated as recoverable and retried, see the README's three lines of defence, item 2); the last_err in the returned error names the authentication failure.
type PublisherOptions ¶
type PublisherOptions struct {
Options
// MaxPayload > 0 rejects messages larger than that many bytes outright
// (returns ErrOversized and counts towards Dropped).
// 0 means no application-level limit (the server still enforces its own max_payload).
MaxPayload int
}
PublisherOptions configures one publishing connection.
type PublisherPool ¶
type PublisherPool struct {
// contains filtered or unexported fields
}
PublisherPool is a pool of publishers. The plain constructor (NewPublisherPool) builds N connections to one endpoint: round-robin start position, failover one connection at a time, nil as soon as any one of them accepts the message. The mirrored constructors (NewMirroredPublisher, NewMirroredPublisherPool) build connections to each of M independent endpoints: every endpoint is an independent send line with its own bounded queue and goroutine, Publish hands one shared copy of the message to each queue without blocking and returns nil as soon as any queue accepts (multi-entry racing on the send side, deduplication on the consumer side). Both constructors return the same type and are used identically apart from construction. While one connection is in a self-healing rebuild, traffic automatically lands on the remaining connections of that endpoint, so 1/N of the messages are not silently lost.
func NewMirroredPublisher ¶ added in v1.7.0
func NewMirroredPublisher(urls []string, o PublisherOptions) (*PublisherPool, error)
NewMirroredPublisher creates one publishing connection per endpoint, at most maxMirroredPublisherEndpoints (5) endpoints, and returns them as a *PublisherPool whose Publish sends once per endpoint and returns nil as soon as any endpoint accepts (see PublisherPool.Publish). Connections are named <Name>-e<endpoint index>. With a single URL there is nothing to mirror: the result is exactly NewPublisherPool(1, o) for that URL (synchronous Publish, connection named <Name>-0), so a configuration can grow from one endpoint to several without changing the host code. With two or more URLs every endpoint is an independent send line (own bounded queue and goroutine), so one stalled endpoint never delays the others. Use NewMirroredPublisherPool when each endpoint needs several connections.
func NewMirroredPublisherPool ¶
func NewMirroredPublisherPool(urls []string, n int, o PublisherOptions) (*PublisherPool, error)
NewMirroredPublisherPool creates, for each of the M endpoints, a group of n publishing connections on the same topic (not yet connected; call Start). Connections are named <Name>-e<endpoint index>-<member index>. Each endpoint is an independent send line (own queue and goroutine); Publish semantics are described in the mirroring paragraph of PublisherPool.Publish. With a single URL there is nothing to mirror and the result is exactly NewPublisherPool(n, o) for that URL (members named <Name>-<i>, synchronous Publish).
func NewPublisherPool ¶
func NewPublisherPool(n int, o PublisherOptions) (*PublisherPool, error)
NewPublisherPool creates n publishing connections on the same topic (not yet connected; call Start). Each connection's client name is <Name>-<i>.
func (*PublisherPool) ConnectedCount ¶
func (p *PublisherPool) ConnectedCount() int
ConnectedCount returns the number of currently connected connections in the pool (for observing degradation).
func (*PublisherPool) Dropped ¶
func (p *PublisherPool) Dropped() uint64
Dropped returns the cumulative count of messages for which every endpoint failed and the whole message was discarded. In a mirrored pool this is settled asynchronously, once the last endpoint's send goroutine has handled the message (or dropped it at the queue). This is the pool's true message-loss count: a single connection's Stats.Dropped is the number of failed attempts on that connection, and the message may well have been failed over to another connection (or, under mirroring, another endpoint) and sent successfully, so summing the members grossly overstates loss (false alarms).
func (*PublisherPool) Endpoints ¶
func (p *PublisherPool) Endpoints() int
Endpoints returns the number of endpoints the pool covers (len(urls) for the mirrored constructor, always 1 for the plain one).
func (*PublisherPool) MustStart ¶
func (p *PublisherPool) MustStart()
MustStart is Start but panics on failure. Suited to fail-fast startup.
func (*PublisherPool) Publish ¶
func (p *PublisherPool) Publish(data []byte) error
Publish sends one message. With the plain constructor it amounts to "two routing passes over the pool, nil as soon as one connection succeeds"; with the mirrored constructor the same routing runs once per endpoint (one send each) and it returns nil as soon as any endpoint accepts. Mirroring is racing redundancy: a single endpoint failing merely loses one path (the message was delivered via the others) and is not counted as loss. Only when every endpoint fails does it return an error (errors.Join) and count towards the pool-level Dropped.
The two routing passes within an endpoint: the first pass tries only members that are currently connected (IsConnected), so the message leaves immediately on a healthy connection and never sits in a disconnected member's reconnect buffer until that member reconnects (latency first: a message delivered seconds late is about as good as undelivered, while a healthy connection was sitting idle right next to it). The second pass turns to the disconnected members: when no member of the endpoint is healthy it falls back to "written into the reconnect buffer counts as success", so the message is buffered rather than dropped.
Mirrored pools (two or more endpoints) do not send on the caller's goroutine at all: one copy of data is handed to every endpoint's bounded send queue (non-blocking) and nil means at least one queue accepted it; the endpoint goroutines then run the routing above independently, so a stalled endpoint only backs up its own queue (its copies are dropped once the queue is full, counted on that endpoint) while the others keep sending. Send failures surface asynchronously in the members' Stats, and a message that every endpoint failed to send still counts towards Dropped. ErrQueueFull is returned when no endpoint could take the message.
Safe for concurrent use. data is copied before Publish returns, so the caller may reuse it immediately.
func (*PublisherPool) Size ¶
func (p *PublisherPool) Size() int
Size returns the total connection count of the pool (mirrored constructor: endpoints x n per endpoint).
func (*PublisherPool) Start ¶
func (p *PublisherPool) Start() error
Start starts every connection in the pool; if any fails (configuration error) the ones already started are stopped and that error is returned.
Members are dialled with bounded concurrency (maxConcurrentDials): a slow handshake on one endpoint's members no longer postpones the dial of an independent endpoint by their count times the dial timeout.
func (*PublisherPool) Stats ¶
func (p *PublisherPool) Stats() []Stats
Stats returns a statistics snapshot per connection in the pool, flattened by member (with the mirrored constructor, endpoint 0's members come first, in urls order; each entry's Name carries the -e<endpoint>-<member> suffix for attribution). Note that each entry's Dropped includes intermediate failover failures; to monitor message loss use the pool-level Dropped() (see its documentation). In a mirrored pool every member of an endpoint also reports that endpoint's send-queue level in QueueLen / QueueCap and the copies its queue discarded in QueueDropped, so a stalled endpoint shows as a filling queue and then as a growing QueueDropped.
func (*PublisherPool) Stop ¶
func (p *PublisherPool) Stop()
Stop stops every connection in the pool. Members are closed in parallel: each blocks at most stopGrace (see link.stop), and the parallelism makes the pool-level worst case max rather than the sum, so the shutdown budget (e.g. systemd TimeoutStopSec) does not shrink linearly with pool size.
func (*PublisherPool) WaitConnected ¶
func (p *PublisherPool) WaitConnected(timeout time.Duration) error
WaitConnected blocks until every connection in the pool is ready; on timeout it returns the error of the first connection that was not ready. Usage is described on Publisher.WaitConnected.
type Stats ¶
type Stats struct {
Name string // Connection name (Options.Name; pool members carry a -<i> suffix, mirror endpoints a -e<i> suffix). Used to attribute a degraded connection.
Running bool // true between Start and Stop.
Connected bool // Whether currently connected.
Status string // Underlying connection status (CONNECTED / RECONNECTING / ...); "no-conn" when there is no connection.
Server string // Name of the currently connected server.
Cluster string // Name of the currently connected cluster.
Reconnects uint64 // Successful client automatic reconnects (RECONNECTING -> CONNECTED).
Rebuilds uint64 // Whole-connection self-healing rebuilds after a "permanent close" (excluding the initial connect).
InMsgs uint64 // Messages received.
OutMsgs uint64 // Messages sent.
InBytes uint64 // Bytes received.
OutBytes uint64 // Bytes sent.
// Dropped is, for a Publisher, the number of failed publishes (rebuild
// window / buffer overflow); for a Subscriber, the number of messages
// dropped by back-pressure because the worker queue was full. Only the
// worker-pool mode increments it; the synchronous and go-per-message modes
// keep it at 0 (in synchronous mode the data-loss signal is SlowConsumers;
// go-per-message never drops and has no back-pressure signal either, see
// the trade-off notes on SubscriberOptions.Workers).
// Note: inside a PublisherPool, a single connection's Dropped counts
// "attempts that failed on this connection"; the message may well have
// been failed over to another connection and sent. To monitor the pool's
// real losses use PublisherPool.Dropped(); summing the members' Dropped
// grossly overestimates loss (false alarms).
Dropped uint64
// Paused reports whether the subscriber is paused (after Subscriber.Pause,
// before Resume): the connection is up but the subscription is gone, so
// the server delivers nothing to this connection. Always false for a
// Publisher.
Paused bool
// Pauses is the cumulative number of successful Pause transitions on the
// subscriber (idempotent repeated calls are not counted). Always 0 for a
// Publisher.
Pauses uint64
// Diverged is the number of subscriber endpoints whose subscription state
// disagrees with the paused state (v1.5.1): not paused but no subscription
// on the connection (SUB failed during Resume), or paused but the
// subscription is still there (UNSUB failed during Pause). Only endpoints
// whose connection is alive are counted (an endpoint inside its rebuild
// window is handled by rebuild; look at Connected instead). While it is
// >0 the package's reconcile goroutine is retrying with backoff; if it
// stays >0 the reconcile is not succeeding, see the "[natslink]
// subscription reconcile failed" log line. Always 0 for a Publisher.
Diverged int
// Gated is the number of messages the subscriber dropped because the gate
// (SubscriberOptions.Gate returned false) rejected them. The decision is
// made at the delivery entry point, before the queue and the callback, so
// all three consumption modes count it; always 0 when no Gate is set.
// Under a mirrored subscriber it is, like Dropped, a single value shared
// across endpoints and likewise counted per copy.
Gated uint64
// Deduped is the number of messages the subscriber dropped at the delivery
// entry point because SubscriberOptions.Dedup had already seen a
// byte-identical payload (one per dropped copy; under mirroring the
// expected steady-state value is (endpoints-1) per message). Always 0
// without a DedupSet and for publishers.
Deduped uint64
// SlowConsumers is the number of times the subscriber was too slow and the
// NATS client discarded messages (ErrSlowConsumer). The connection still
// looks healthy at that point, so this counter is the key signal for
// detecting "silent data loss" (in synchronous mode it is the only
// signal). It counts events, not messages, and must not be added to
// Dropped as a total loss figure.
SlowConsumers uint64
QueueLen int // Current length of the Subscriber worker queue (always 0 in synchronous / go-per-message mode); for a mirrored pool member, its endpoint's send-queue length.
QueueCap int // Capacity of the Subscriber worker queue (always 0 in synchronous / go-per-message mode); for a mirrored pool member, its endpoint's send-queue capacity.
// QueueDropped is, for a member of a mirrored pool, the number of copies its
// endpoint's send queue discarded: queue full (entries or bytes) or still
// queued when Stop gave up on a stalled send goroutine. Send failures are
// not included (they are this member's Dropped). Always 0 for subscribers
// and plain publishers.
QueueDropped uint64
}
Stats is a point-in-time snapshot of status and data statistics. All counters accumulate across self-healing rebuilds. At the instant of a rebuild switch-over a counter may briefly appear to go backwards (the new connection has been swapped in but the old connection's statistics have not been absorbed yet); when computing deltas for monitoring, clamp negative increments to zero.
type Subscriber ¶
type Subscriber struct {
// contains filtered or unexported fields
}
Subscriber is a self-supervising subscription to a single topic. The plain constructor (NewSubscriber) holds one connection; the mirrored constructor (NewMirroredSubscriber) holds one connection per endpoint sharing a single worker pool and callback, and Handler receives every copy from every endpoint (deduplication is the consumer's job). Both constructors return the same type and are used identically apart from construction. Usage: NewSubscriber / NewMirroredSubscriber -> Start -> (Stats/IsConnected at any time; Pause/Resume to suspend and re-subscribe without disconnecting) -> Stop.
func NewMirroredSubscriber ¶
func NewMirroredSubscriber(urls []string, o SubscriberOptions) (*Subscriber, error)
NewMirroredSubscriber creates one subscribing connection per endpoint (not yet connected; call Start), all sharing a single worker pool and callback: the Handler receives every copy from every endpoint, and deduplication is the caller's job; Workers is the total concurrency bound across endpoints. Connections are named <Name>-e<endpoint index>. Note that SyncMode is no longer serial under a mirror (M delivery goroutines call the callback concurrently, see the Handler comment); the remaining concurrency / ordering / statistics semantics are documented on Handler and Subscriber.Stats. With a single URL there is nothing to mirror and the result is exactly NewSubscriber for that URL (plain name, no -e0).
func NewSubscriber ¶
func NewSubscriber(o SubscriberOptions) (*Subscriber, error)
NewSubscriber creates a subscription connection (not yet connected; call Start).
func (*Subscriber) Conn ¶
func (s *Subscriber) Conn() *nats.Conn
Conn returns the first endpoint's underlying connection, which may be nil during a rebuild window. Escape hatch only (e.g. Request-Reply); under mirroring it exposes only the endpoint for the first url.
func (*Subscriber) Endpoints ¶
func (s *Subscriber) Endpoints() int
Endpoints returns the endpoint count (len(urls) for the mirrored constructor, always 1 for the plain one).
func (*Subscriber) IsConnected ¶
func (s *Subscriber) IsConnected() bool
IsConnected reports whether every endpoint is connected (false while RECONNECTING or inside a rebuild window). Under mirroring any endpoint being down yields false: endpoint degradation must be visible (the race has degraded to a single path).
func (*Subscriber) IsPaused ¶
func (s *Subscriber) IsPaused() bool
IsPaused reports whether the subscription is currently paused (between Pause and Resume).
func (*Subscriber) MustStart ¶
func (s *Subscriber) MustStart()
MustStart is Start but panics on failure.
Note: MustStart succeeding does not mean connected (it succeeds even when the server is unreachable; retries continue in the background). For fail-fast "die if it cannot connect" semantics at startup, call MustProbe first (the standard pattern is in the Probe documentation; for the mirrored constructor probe each endpoint URL separately).
func (*Subscriber) Pause ¶
func (s *Subscriber) Pause() error
Pause suspends the subscription while keeping the connections open: it sends UNSUB on each endpoint's current connection, after which the server stops delivering this topic to the connection. Interest is withdrawn hop by hop along leaf and route links, so upstream NATS servers stop forwarding the stream to this node as well: the saving is real bandwidth, not a local discard (contrast Gate, which drops at the delivery entry point while the bytes still arrive on the NIC). Typical use: a host that knows it will not need this stream for a long stretch. The connection itself, heartbeats, automatic reconnect and self-healing rebuilds all continue, and Stats.Connected is unaffected; meanwhile Stats.Paused is true and Stats.Pauses is incremented once.
Semantics and edges:
- Idempotent: calling it while already paused returns nil. After Stop it returns ErrStopped.
- A self-healing rebuild during the pause still swaps in the new connection, it just does not subscribe on it; a nats.go automatic reconnect (RECONNECTING -> CONNECTED) replays only the subscriptions still on the books, so an UNSUBed one does not come back. Neither path needs caller intervention.
- Messages already in flight before the UNSUB still arrive and are delivered (the server takes a moment to process the UNSUB), and in worker-pool mode messages already queued but not yet consumed are still delivered to the callback; callers that need strict point-in-time semantics must re-check inside Handler (the same rule as Gate's queue residency window).
- An endpoint inside a rebuild window (connection permanently closed, new one not yet swapped in) has nothing to do and is not an error: the new connection is handled as paused when it is swapped in.
- The returned error is the join (errors.Join) of per-endpoint Unsubscribe failures; "connection already closed" and "handle already invalid", both meaning there was no subscription to begin with, are not counted as errors.
- When an endpoint's UNSUB fails, its handle stays on the books and the paused state still takes effect; the package's reconcile goroutine retries with backoff until the UNSUB succeeds (or the connection closes and the rebuild takes over), logging "natslink unsubscribe retried after failed pause"; until it converges Stats.Diverged > 0. The caller does not need to retry.
Unsubscribe takes the underlying connection lock and can block up to flusherTimeout (5s) if the peer is wedged; do not call it synchronously on a latency-sensitive path.
func (*Subscriber) Resume ¶
func (s *Subscriber) Resume() error
Resume re-subscribes: it sends SUB again on each endpoint's most recently set-up connection, after which messages flow to the same Handler / worker pool. Idempotent: returns nil when not paused; returns ErrStopped after Stop.
An endpoint whose connection is permanently closed (inside a rebuild window) is skipped: the setup of the rebuilt connection subscribes it as "not paused", so the caller need not retry. A connection in RECONNECTING has its SUB queued by nats.go and applied on reconnect, which is likewise not an error. The returned error is the join of per-endpoint subscribe failures; when one endpoint fails the others still resume and the paused state is already cleared (Stats.Paused is false). The failed endpoint is retried with backoff by the package's reconcile goroutine until its SUB succeeds (before that existed, an endpoint whose connection stayed healthy and never rebuilt would remain unsubscribed forever), logging "natslink subscribe retried after failed resume"; until it converges Stats.Diverged > 0. The caller does not need to retry.
func (*Subscriber) Start ¶
func (s *Subscriber) Start() error
Start opens every endpoint connection and subscribes. A temporarily unreachable server is not an error (it retries in the background until connected, and the subscription takes effect with the connection). An error from any endpoint (essentially only configuration errors such as a bad URL or topic, which should be treated as fatal) rolls back the endpoints already started.
The worker pool is started before any endpoint is dialled, so an endpoint that connects early never delivers into a queue nobody drains while another endpoint is still dialling; endpoints are dialled with bounded concurrency (maxConcurrentDials), so startup takes the slowest batch, not the sum.
func (*Subscriber) Stats ¶
func (s *Subscriber) Stats() Stats
Stats returns a statistics snapshot merged across endpoints; safe to call concurrently at any time. Merge rules:
- Name is the first endpoint's connection name (plain constructor: the connection name itself);
- Connected is true only when every endpoint is connected (endpoint degradation shows up immediately as not connected, so a host's "degraded" alert covers it for free); Status/Server/Cluster come from the first connected endpoint, or from the first endpoint when none is connected;
- counters (Reconnects/Rebuilds/In*/Out*/SlowConsumers) are summed across endpoints;
- Dropped is the worker-queue overflow count (the queue is shared, so it is a single value anyway). Note that under mirroring each message has M copies, each taking one queue slot, so under overload Dropped counts copies (roughly M times amplified); content is lost only when every copy of a message is dropped, so divide by the multiplier before panicking about Dropped > 0;
- for the plain (single-endpoint) constructor every field matches the previous behaviour.
func (*Subscriber) Stop ¶
func (s *Subscriber) Stop()
Stop closes every endpoint connection, ends the worker goroutines, and blocks until all in-flight callbacks have returned. After Stop returns it is safe to release the resources the callback depends on (DB handles, channels, ...) without racing a straggling callback. Bounded promise: closing a single endpoint connection waits at most stopGrace (5s); if the peer is wedged the wait is abandoned and the close continues in the background (see link.stop), so in extreme cases the connection may not be fully closed when Stop returns. The GoPerMessage/SyncMode dispatch carries a done gate, so no new callbacks start after Stop (apart from the single-message race window with a callback already in flight). Callbacks must therefore terminate normally; a permanently blocked callback wedges Stop. Messages still queued and not yet picked up are discarded. The wait guarantee covers worker-pool mode only: in synchronous mode (SyncMode) the callback runs on the NATS delivery goroutine, and in go-per-message mode (GoPerMessage) on detached goroutines, and Stop waits for neither. A go-per-message user that wants to release callback resources after Stop must confirm for itself that in-flight callbacks have finished (or shut down in reverse dependency order so that the dependency outlives this subscription). Stop is idempotent and safe to call repeatedly; the instance is not reusable afterwards.
func (*Subscriber) Topic ¶
func (s *Subscriber) Topic() string
Topic returns the topic this subscription is bound to.
func (*Subscriber) WaitConnected ¶
func (s *Subscriber) WaitConnected(timeout time.Duration) error
WaitConnected blocks until every endpoint connection is ready; on timeout it returns the error of the first endpoint that was not ready. Usage and caveats are in the link.WaitConnected / Probe documentation.
type SubscriberOptions ¶
type SubscriberOptions struct {
Options
Handler Handler // Required. Message callback (concurrency contract on the Handler type).
// Gate is a callback admission check. When non-nil it is called once per
// message at the delivery entry point; the message continues to Handler only
// if it returns true, otherwise it is dropped on the spot and counted
// (Stats.Gated). nil means always admit (the previous behaviour). Typical
// use: a host that is not yet ready to process messages (still warming up,
// still catching up on state) gates everything off, and admission resumes
// automatically once it is ready, without rebuilding the subscription.
//
// Contract: Gate runs on the NATS delivery goroutine (under mirroring, on
// each endpoint's own delivery goroutine, concurrently), so it must be
// concurrency-safe and extremely cheap (an atomic read; a slow Gate blocks
// delivery and triggers slow-consumer drops). The decision is made before
// enqueue/dispatch: in worker-pool mode a message that was admitted and
// queued, but not yet picked up by a worker when the gate closes, is still
// delivered to the callback (queue residency window). Callers that need
// strict point-in-time semantics must re-check inside Handler.
Gate func() bool
// Dedup, when non-nil, drops a message whose payload is byte-identical to
// one the set has recorded recently (Stats.Deduped) before it is queued or
// dispatched. It is a general byte-level filter for any subscriber: the
// same set may be given to one subscriber, to several (identical payloads
// on different subjects then collapse as well, e.g. one event published on
// three feeds), or to every subscriber a process owns, and the host may
// also call DedupSet.Seen directly on paths that do not go through a
// Subscriber. The typical case is a mirrored subscription, where the M
// endpoint copies of one message collapse into a single callback without
// decoding and the first copy to arrive wins. nil means every message is
// delivered (the previous behaviour).
//
// The check runs after Gate and before the dispatch mode, on the NATS
// delivery goroutine: one hash of the payload (outside the set's lock) plus
// a short locked map lookup. Messages with an empty payload bypass it.
// Only byte-identical copies are collapsed; two messages carrying the same
// content in different bytes are not duplicates to the set, so an
// application that needs content-level deduplication keeps it. The window
// is a capacity (and optionally an age), not a guarantee: a copy arriving
// after the set has forgotten the first one is delivered again.
Dedup *DedupSet
// DedupKey, when non-nil, replaces the payload hash with a caller-computed
// 64-bit key for the Dedup check (for example a hash already present in the
// payload at a fixed offset, saving the hash of a large message). Returning
// ok=false delivers the message without consulting the set. Requires Dedup;
// runs on the delivery goroutine, so it must be cheap and concurrency-safe.
DedupKey func(msg *nats.Msg) (key uint64, ok bool)
// Workers is the worker-pool goroutine count (bounded concurrency), default 8.
// The callback never blocks the NATS delivery goroutine; when the pool cannot
// keep up, new messages are dropped and counted (Stats.Dropped, so backpressure
// is visible), which avoids the goroutine explosion / OOM that go-per-message
// suffers under backpressure.
// Workers = SyncMode (-1) selects synchronous mode: no worker pool, the
// callback runs directly on the NATS delivery goroutine. Lowest latency (one
// queue hand-off saved) but only suitable for non-blocking, very light
// processing (a callback with lock contention, IO or millisecond-scale compute
// belongs in the worker pool).
// Workers = GoPerMessage (-2) selects go-per-message mode: no worker pool, the
// delivery goroutine spawns a new goroutine per message, with unbounded
// concurrency. Suited to streams where most messages are light but a few do
// heavy synchronous work and must not be dropped. A bounded pool on such mixed
// traffic gets its workers filled by the heavy messages, head-of-line blocks
// the light ones, and drops under bursts, while the throughput ceiling of such
// callbacks is usually a downstream lock, so extra goroutines merely queue on
// it. The cost (read before choosing it): this mode has no backpressure signal
// at all (Dropped stays 0; the callback never blocks delivery, so
// SlowConsumers hardly ever fires either), the goroutine count floats with
// load, and the caller must know their upstream rate and downstream capacity.
// Any other negative value is a configuration error (NewSubscriber returns an error).
// A mirrored subscription shares one worker pool: Workers is the total
// concurrency bound across endpoints, not multiplied per endpoint.
Workers int
// QueueSize is the worker-pool buffer length (backpressure bound), default 4096.
// No negative-value semantics; <= 0 always means the default.
// A mirrored subscription shares one queue.
QueueSize int
// QueueGroup, when non-empty, makes this a queue subscription: subscribers in
// the same group load-balance and compete for messages (each message goes to
// one member). Unset means broadcast: every Subscriber receives every message.
// For replicated deployments leave it unset to process everything on every
// replica, set it to share the load.
// Under mirroring the queue-group semantics apply per endpoint: members of a
// group compete only within one endpoint, and each endpoint still delivers
// its own copy.
QueueGroup string
// NATS client subscription buffer limits (another layer of protection before
// this package's callback). When exceeded the client drops messages and raises
// ErrSlowConsumer (counted in Stats.SlowConsumers). Essentially only reached in
// synchronous mode with a slow callback (or an extreme burst): in worker-pool
// mode backpressure is governed by Workers/QueueSize, and these two normally
// need no tuning. Under mirroring they apply per endpoint.
PendingMsgLimit int // 0 uses the nats default (512k messages); -1 means unlimited
PendingBytesLimit int // 0 uses the nats default (64MB); -1 means unlimited
}
SubscriberOptions configures one subscription connection.