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
- Variables
- func EnvironmentPrefix(environmentID int64) string
- func FlagKey(environmentID int64, flagKey string) string
- func LocalizationKey(environmentID, microserviceID int64, locale string) string
- func MicroKey(environmentID, microserviceID int64) string
- type BucketOptions
- type Buckets
- type Client
- type Config
- type ConfigPublisher
- type FlagPayload
- type KeyRevision
- type NoopPublisher
- func (NoopPublisher) DeleteKey(context.Context, string, string) error
- func (NoopPublisher) EnsureBuckets(context.Context) error
- func (NoopPublisher) ListKeys(context.Context, string) ([]string, error)
- func (NoopPublisher) ListRevisions(context.Context, string) ([]KeyRevision, error)
- func (NoopPublisher) PublishFlag(context.Context, int64, string, FlagPayload) error
- func (NoopPublisher) PublishLocalization(context.Context, int64, int64, string, json.RawMessage) error
- func (NoopPublisher) PublishMicroConfig(context.Context, int64, int64, json.RawMessage) error
- type Publisher
- func (p *Publisher) DeleteKey(ctx context.Context, bucket, key string) error
- func (p *Publisher) EnsureBuckets(ctx context.Context) error
- func (p *Publisher) ListKeys(ctx context.Context, bucket string) ([]string, error)
- func (p *Publisher) ListRevisions(ctx context.Context, bucket string) ([]KeyRevision, error)
- func (p *Publisher) PublishFlag(ctx context.Context, environmentID int64, flagKey string, payload FlagPayload) error
- func (p *Publisher) PublishLocalization(ctx context.Context, environmentID, microserviceID int64, locale string, ...) error
- func (p *Publisher) PublishMicroConfig(ctx context.Context, environmentID, microserviceID int64, ...) error
- type ReconcileSource
- type Reconciler
- type ResyncResult
Constants ¶
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.
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 ¶
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 ¶
EnvironmentPrefix is what a consumer watches to receive every key for its environment. e.g. EnvironmentPrefix(3) -> "3.>"
func FlagKey ¶
FlagKey builds the FLAGS bucket key: {environmentID}.{flagKey} e.g. FlagKey(3, "search_v2") -> "3.search_v2"
func LocalizationKey ¶
LocalizationKey builds the LOCALIZATION bucket key: {environmentID}.{microserviceID}.{locale} e.g. LocalizationKey(3, 42, "pt-BR") -> "3.42.pt-BR"
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 ¶
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 ¶
Client bundles a live NATS connection with its JetStream context.
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 KeyRevision ¶
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) EnsureBuckets ¶
func (NoopPublisher) EnsureBuckets(context.Context) error
func (NoopPublisher) ListRevisions ¶
func (NoopPublisher) ListRevisions(context.Context, string) ([]KeyRevision, error)
func (NoopPublisher) PublishFlag ¶
func (NoopPublisher) PublishFlag(context.Context, int64, string, FlagPayload) error
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 (*Publisher) ListRevisions ¶
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 (*Publisher) PublishLocalization ¶
func (*Publisher) PublishMicroConfig ¶
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.