messaging

package
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 7, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package messaging is the distribution plane: the NATS connection, the JetStream KV buckets (FLAGS, MICROCONFIG, LOCALIZATION), the key layout consumers watch, the write-through publisher, and the reconciler.

Keys are environment-prefixed — "{envID}.…" — so a consumer can watch one prefix and receive every change relevant to it. keys.go is the authority on that shape; consumers in other languages build the same keys by hand from docs/CONSUMER_CONTRACT.md, so changing it is a contract change.

The dual write is not transactional. A publish can fail after the database has committed, which is what the reconciler exists for: it periodically resweeps each domain, republishes what KV is missing or stale, and prunes keys whose rows are gone. A sweep that only partially succeeds neither prunes nor advances its window, so it retries rather than losing ground.

Index

Constants

View Source
const (
	BucketFlags        = "FLAGS"
	BucketMicroConfig  = "MICROCONFIG"
	BucketLocalization = "LOCALIZATION"
)

KV bucket names. These are the live "get-latest + watch" surfaces that consuming microservices read from: one bucket per config domain, each holding the current value for every key and pushing every change.

View Source
const MaxValueSize = 512 << 10 // 512 KiB

MaxValueSize bounds a single KV value. Left unset a bucket inherits the server's max_payload (1 MB by default), so a payload the admin API accepted with a 201 could be refused by JetStream forever afterwards — drift the reconciler can never heal because every sweep fails on the same row. The domain services refuse anything larger before the database write, which is why this number and the one they check against have to be the same one.

Variables

View Source
var ErrNotProvisioned = errors.New("messaging: KV buckets are not provisioned")

ErrNotProvisioned is returned by a publish attempted before the buckets exist: NATS was unreachable at startup and Ensure has not yet succeeded. Distinct so a caller can tell it apart from a rejected value.

Functions

func EnvironmentPrefix

func EnvironmentPrefix(environmentID int64) string

EnvironmentPrefix is what a consumer watches to receive every key for its environment. e.g. EnvironmentPrefix(3) -> "3.>"

func FlagKey

func FlagKey(environmentID int64, flagKey string) string

FlagKey builds the FLAGS bucket key: {environmentID}.{flagKey} e.g. FlagKey(3, "search_v2") -> "3.search_v2"

func LocalizationKey

func LocalizationKey(environmentID, microserviceID int64, locale string) string

LocalizationKey builds the LOCALIZATION bucket key: {environmentID}.{microserviceID}.{locale} e.g. LocalizationKey(3, 42, "pt-BR") -> "3.42.pt-BR"

func MicroKey

func MicroKey(environmentID, microserviceID int64) string

MicroKey builds the MICROCONFIG bucket key: {environmentID}.{microserviceID} e.g. MicroKey(3, 42) -> "3.42"

Types

type BucketOptions

type BucketOptions struct {
	History  uint8 // number of historical values kept per key (rollback depth)
	Replicas int   // 1 for dev, 3 for prod cluster
}

BucketOptions controls how buckets are provisioned. Defaults are safe for a single-node dev NATS; set Replicas=3 and Storage=file for a prod cluster.

type Buckets

type Buckets struct {
	// contains filtered or unexported fields
}

Buckets holds the three KV stores this service publishes to. Handles are swapped in place by Ensure, so every read goes through the mutex.

func EnsureBuckets

func EnsureBuckets(ctx context.Context, js jetstream.JetStream, opts BucketOptions) (*Buckets, error)

EnsureBuckets creates (or updates) the FLAGS, MICROCONFIG and LOCALIZATION KV buckets idempotently. Safe to call on every startup.

func NewBuckets

func NewBuckets(js jetstream.JetStream, opts BucketOptions) *Buckets

NewBuckets returns a Buckets whose handles are not provisioned yet. It is what makes a NATS outage at startup non-fatal: every publish fails cleanly against it while the database-backed read paths carry on serving, and the reconciler's per-cycle Ensure fills the handles in as soon as NATS is back.

func (*Buckets) Ensure

func (b *Buckets) Ensure(ctx context.Context) error

Ensure re-provisions the buckets and swaps in the fresh handles. It exists so the service heals itself when NATS comes back with an empty store: startup provisioning alone would leave every publish failing forever. Idempotent and cheap — CreateOrUpdateKeyValue is a no-op when the bucket already matches — so it is safe to call at the start of every reconcile cycle.

type Client

type Client struct {
	Conn *nats.Conn
	JS   jetstream.JetStream
}

Client bundles a live NATS connection with its JetStream context.

func Connect

func Connect(cfg Config) (*Client, error)

Connect dials NATS and returns a JetStream handle. The connection is configured to reconnect indefinitely so a NATS blip does not take the service down; consumers reading from KV keep serving cached values anyway.

func (*Client) Drain

func (c *Client) Drain() error

Drain gracefully flushes and closes the connection. Safe on a nil client.

type Config

type Config struct {
	URL           string // e.g. nats://nats:4222 or comma-separated cluster URLs
	CredsFile     string // optional path to a NATS .creds file (NKEY/JWT)
	Name          string // connection name, shown in NATS monitoring
	MaxReconnect  int    // -1 = unlimited
	ReconnectWait time.Duration
}

Config holds the NATS/JetStream connection settings. All values come from the application Config (never hardcoded).

type ConfigPublisher

type ConfigPublisher interface {
	PublishFlag(ctx context.Context, environmentID int64, flagKey string, payload FlagPayload) error
	PublishMicroConfig(ctx context.Context, environmentID, microserviceID int64, settings json.RawMessage) error
	PublishLocalization(ctx context.Context, environmentID, microserviceID int64, locale string, bundle json.RawMessage) error

	// ListKeys returns every key currently held in the named bucket. A bucket
	// that is empty or does not exist yields no keys and no error.
	ListKeys(ctx context.Context, bucket string) ([]string, error)
	// ListRevisions returns the same keys with the revision each one is
	// currently at. The reconciler needs the revisions to tell a key that has
	// sat untouched since its sweep began from one written during it.
	ListRevisions(ctx context.Context, bucket string) ([]KeyRevision, error)
	// DeleteKey removes a key from the named bucket. This is how a row deleted
	// from the database stops being served: without it the KV entry survives
	// forever and consumers keep the stale value in memory.
	DeleteKey(ctx context.Context, bucket, key string) error
	// EnsureBuckets re-provisions the buckets, so a NATS restart with an empty
	// store does not leave every publish failing until this process restarts.
	EnsureBuckets(ctx context.Context) error
}

type FlagPayload

type FlagPayload struct {
	Enabled   bool   `json:"enabled"`
	Value     string `json:"value"`
	UpdatedAt string `json:"updatedAt"` // RFC3339
}

type KeyRevision

type KeyRevision struct {
	Key      string
	Revision uint64
}

KeyRevision pairs a KV key with the revision of the value it holds. The revision is a per-bucket sequence, so comparing two readings of it is how a caller decides whether a key changed between them without trusting a clock.

type NoopPublisher

type NoopPublisher struct{}

NoopPublisher satisfies ConfigPublisher but does nothing. Used when PUBLISH_ENABLED=false — the API still serves reads and writes against the database, it just does not distribute anything — and in tests.

func (NoopPublisher) DeleteKey

func (NoopPublisher) EnsureBuckets

func (NoopPublisher) EnsureBuckets(context.Context) error

func (NoopPublisher) ListKeys

func (NoopPublisher) ListKeys(context.Context, string) ([]string, error)

func (NoopPublisher) ListRevisions

func (NoopPublisher) ListRevisions(context.Context, string) ([]KeyRevision, error)

func (NoopPublisher) PublishFlag

func (NoopPublisher) PublishLocalization

func (NoopPublisher) PublishLocalization(context.Context, int64, int64, string, json.RawMessage) error

func (NoopPublisher) PublishMicroConfig

func (NoopPublisher) PublishMicroConfig(context.Context, int64, int64, json.RawMessage) error

type Publisher

type Publisher struct {
	// contains filtered or unexported fields
}

func NewPublisher

func NewPublisher(b *Buckets) *Publisher

func (*Publisher) DeleteKey

func (p *Publisher) DeleteKey(ctx context.Context, bucket, key string) error

func (*Publisher) EnsureBuckets

func (p *Publisher) EnsureBuckets(ctx context.Context) error

func (*Publisher) ListKeys

func (p *Publisher) ListKeys(ctx context.Context, bucket string) ([]string, error)

func (*Publisher) ListRevisions

func (p *Publisher) ListRevisions(ctx context.Context, bucket string) ([]KeyRevision, error)

ListRevisions walks the bucket's current values metadata-only, so a sweep over thousands of keys costs one pass and none of the payloads.

func (*Publisher) PublishFlag

func (p *Publisher) PublishFlag(ctx context.Context, environmentID int64, flagKey string, payload FlagPayload) error

func (*Publisher) PublishLocalization

func (p *Publisher) PublishLocalization(ctx context.Context, environmentID, microserviceID int64, locale string, bundle json.RawMessage) error

func (*Publisher) PublishMicroConfig

func (p *Publisher) PublishMicroConfig(ctx context.Context, environmentID, microserviceID int64, settings json.RawMessage) error

type ReconcileSource

type ReconcileSource interface {
	// Name identifies the source in logs.
	Name() string
	// Bucket is the KV bucket this source owns. On a complete full sweep the
	// keys Resync returned are taken as the contents of that bucket and
	// everything else in it is a candidate for deletion.
	Bucket() string
	// Resync publishes rows changed at or after `since` and reports the KV keys
	// it published. A zero `since` means "publish everything" (full sweep). An
	// error means the source could not be read at all; a row that could not be
	// published is recorded on the result instead, so one bad row does not
	// strand the ones behind it.
	Resync(ctx context.Context, pub ConfigPublisher, since time.Time) (ResyncResult, error)
}

ReconcileSource re-publishes a domain's current database state to KV. Called on startup and on an interval so KV eventually matches the database even if a write-through publish was dropped (the dual-write healing mechanism).

Implementations live in the app layer (they read the database); messaging stays decoupled from the domains to avoid an import cycle. Resync must be idempotent — the publisher reads before writing, so republishing a value KV already holds is skipped rather than pushed to every consumer.

type Reconciler

type Reconciler struct {
	// contains filtered or unexported fields
}

Reconciler periodically resyncs every registered source.

func NewReconciler

func NewReconciler(interval time.Duration, pub ConfigPublisher, sources ...ReconcileSource) *Reconciler

func (*Reconciler) Start

func (r *Reconciler) Start(ctx context.Context) (stop func())

Start runs an initial resync, then repeats on the interval until the returned stop function is called (or ctx is cancelled). Non-blocking.

stop waits for the running cycle to unwind. Without that wait the shutdown path drains NATS and closes the database out from under a sweep still in a query, which surfaces as an error log on every rolling restart.

type ResyncResult

type ResyncResult struct {
	// Keys are the KV keys the sweep published successfully.
	Keys []string
	// Partial is set when at least one row could not be published. The rest of
	// the sweep still ran, but the result no longer describes the bucket, so it
	// may neither be pruned against nor advance the incremental window.
	Partial bool
}

ResyncResult is what one source reports back from a sweep.

func (*ResyncResult) Publish

func (r *ResyncResult) Publish(source, key string, err error)

Publish records the outcome of publishing one row. A failure is logged against the key that produced it: the source name alone leaves an operator with a whole domain to search for the row that is wedging convergence.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL