pubsub

package
v0.7.23 Latest Latest
Warning

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

Go to latest
Published: Oct 8, 2026 License: BSD-2-Clause Imports: 7 Imported by: 0

Documentation

Overview

Package pubsub is a tiny message-fanout abstraction with pluggable backends. Standalone deployments use MemoryPubSub (same-process, zero-config); HA deployments can opt into a RESP-protocol backend (Redis or Valkey) for cross-node delivery.

The package is deliberately shaped like pkg/cache: one interface, one in-memory default, one or more opt-in HA backends, one factory. See Service for the entry point.

Index

Constants

This section is empty.

Variables

View Source
var ErrBackendUnavailable = errors.New("pubsub: backend unavailable")

ErrBackendUnavailable is logged (not returned) by Service.NewPubSub when an HA backend is requested but its dependency (a redis.UniversalClient) is nil. The factory then yields a MemoryPubSub with a nil error so a misconfigured HA deployment degrades to single-node behaviour instead of refusing to start. Callers cannot use errors.Is(err, ErrBackendUnavailable) to detect the fallback; inspect the log stream or the Service's configured backend instead.

View Source
var ErrClosed = errors.New("pubsub: closed")

ErrClosed is returned by PubSub operations on a closed instance.

Functions

func NewClient

func NewClient(cfg ClientConfig) (redis.UniversalClient, error)

NewClient builds a redis.UniversalClient from cfg. Returns nil without error when cfg has no addresses, so callers can treat "Redis section omitted" as "no HA pub/sub" without extra branching.

Types

type Backend

type Backend int

Backend selects which PubSub implementation Service.NewPubSub returns. BackendMemory is the zero value and the standalone default.

const (
	// BackendMemory selects MemoryPubSub, a single-process bus.
	// Standalone deployments and tests use this.
	BackendMemory Backend = iota
	// BackendRedis selects a Redis Pub/Sub-backed bus for cross-node
	// delivery in HA mode.
	BackendRedis
	// BackendValkey selects a Valkey Pub/Sub-backed bus. Valkey speaks
	// the same RESP protocol as Redis, so the underlying implementation
	// is shared; the separate value exists so operators configure the
	// backend they actually run and so logs identify it correctly.
	BackendValkey
)

func ParseBackend

func ParseBackend(s string) Backend

ParseBackend maps a config string ("memory"/"redis"/"valkey") to a Backend. Unknown and empty values map to BackendMemory so a missing config falls back to the standalone default without erroring.

func (Backend) String

func (b Backend) String() string

String returns the backend's canonical name, matching the yaml value an operator writes in config.

type ClientConfig

type ClientConfig struct {
	// Addrs lists one or more "<host>:<port>" endpoints. A single entry
	// yields a plain client; multiple entries yield a cluster client,
	// matching redis.UniversalOptions semantics.
	Addrs []string
	// Username and Password carry ACL credentials (Redis 6+ / Valkey)
	// or the single AUTH password on older servers.
	Username string
	Password string
	// DB is the logical database number used by single-node mode
	// (ignored in cluster mode).
	DB int
	// TLS enables TLS for the connection using the system root CAs.
	// Finer-grained control (custom CA, mTLS) belongs behind a dedicated
	// builder; this one is deliberately boring.
	TLS bool
}

ClientConfig captures the fields needed to construct a redis.UniversalClient. Low-level builder input; the deployment-facing yaml-tagged shape lives in pubsub.Config, and cmd/verifier maps between the two.

type Config

type Config struct {
	// Backend selects the RESP backend; "redis" or "valkey". Defaults
	// to "redis" when omitted.
	Backend string `yaml:"backend" validate:"omitempty,oneof=redis valkey" default:"redis"`
	// Addrs lists one or more "<host>:<port>" endpoints. A single entry
	// yields a plain client; multiple entries yield a cluster client.
	Addrs []string `yaml:"addrs" validate:"required,min=1,dive,hostname_port" doc_example:"[\"redis:6379\"]"`
	// Username is the ACL username (Redis 6+ / Valkey). Optional.
	Username string `yaml:"username,omitempty"`
	// Password is the ACL password (Redis 6+ / Valkey) or the single
	// AUTH password on older servers. Optional.
	Password string `yaml:"password,omitempty"`
	// DB is the logical database number used by single-node mode
	// (ignored in cluster mode).
	DB int `yaml:"db" default:"0"`
	// TLS enables TLS for the client connection, using system roots.
	TLS bool `yaml:"tls" default:"false"`
}

Config is the deployment-facing configuration for the pub/sub bus.

Supports Redis and Valkey today (both speak RESP, so the same client serves both; Backend only decides what logs and metrics identify it as). Omitted in standalone deployments; when omitted in HA, notifications stay in-process and do not cross nodes.

type Logger

type Logger interface {
	Error(err error, msg string, keysAndValues ...any)
}

Logger mirrors pkg/cache.Logger: a minimal operational-error sink that both *logger.Log and logr.Logger satisfy. Nil is accepted everywhere and swapped for a silent no-op.

type MemoryPubSub

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

MemoryPubSub is a single-process PubSub implementation: publish and subscribe share the same goroutine-safe map of topic groups. It is the standalone default and the test backend; cross-node delivery is outside its remit.

The lifecycle contract matches the needs of SSE fanout:

  • Publish to a topic with no subscriber is a no-op (does not create a lingering group).
  • A subscriber whose channel is not being drained drops messages rather than back-pressuring the publisher.
  • Publish after Close returns ErrClosed; the per-group close path also makes a racing in-flight Publish observe the torn-down state before it tries to send on closed subscriber channels.

func NewMemoryPubSub

func NewMemoryPubSub() *MemoryPubSub

NewMemoryPubSub returns a ready-to-use in-process PubSub.

func (*MemoryPubSub) Close

func (m *MemoryPubSub) Close() error

Close terminates every topic group and lets outstanding Subscriptions observe end-of-stream on their next read. Idempotent.

func (*MemoryPubSub) HealthProbe

func (m *MemoryPubSub) HealthProbe(_ context.Context) error

HealthProbe returns ErrClosed after Close has run, nil otherwise. The in-process bus has no network to probe.

func (*MemoryPubSub) Publish

func (m *MemoryPubSub) Publish(_ context.Context, topic string, payload []byte) error

Publish fans payload out to every subscriber of topic.

func (*MemoryPubSub) Subscribe

func (m *MemoryPubSub) Subscribe(_ context.Context, topic string) (Subscription, error)

Subscribe returns a fresh Subscription for topic. Each call allocates a new buffered channel; the Subscription owns it until Close.

type PubSub

type PubSub interface {
	// Publish sends payload to every live subscriber of topic. It is
	// a no-op when no subscriber exists. Non-blocking: a slow
	// subscriber loses the message rather than back-pressuring the
	// publisher.
	Publish(ctx context.Context, topic string, payload []byte) error

	// Subscribe returns a Subscription delivering future publishes on
	// topic. The caller MUST Close the returned Subscription when
	// done to release backend resources.
	Subscribe(ctx context.Context, topic string) (Subscription, error)

	// Close releases backend resources and closes every outstanding
	// Subscription. Safe to call multiple times.
	Close() error

	// HealthProbe reports whether the backing bus is reachable. Returns
	// nil when healthy; a non-nil error surfaces unchanged to the
	// aggregator (and to the SSE path, where a dead HA bus would
	// otherwise leave every cross-node notification silently dropped).
	HealthProbe(ctx context.Context) error
}

PubSub is a topic-based publish/subscribe bus. Payloads are opaque bytes; callers choose their own encoding.

Delivery is best-effort fire-and-forget: a message published to a topic with no live subscriber is dropped, and a subscriber whose channel is not being drained drops the message rather than blocking publishers. Durable delivery is a caller concern - see internal/verifier/apiv1 for the completion-marker pattern that handles reload survival for the verifier notify flow.

func NewRedisPubSub

func NewRedisPubSub(client redis.UniversalClient, prefix string, log Logger) (PubSub, error)

NewRedisPubSub creates a Redis-backed PubSub. prefix namespaces every channel name as "<prefix>:<topic>" so multiple buses may share one Redis keyspace without colliding. client ownership stays with the caller.

func NewValkeyPubSub

func NewValkeyPubSub(client redis.UniversalClient, prefix string, log Logger) (PubSub, error)

NewValkeyPubSub creates a Valkey-backed PubSub. Valkey is the Linux-Foundation-maintained Redis fork and speaks the same RESP protocol, so the implementation is shared with NewRedisPubSub; the separate constructor exists so operators pick the backend they actually run and so logs identify it correctly.

type Service

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

Service manages PubSub creation. One Service per process; one PubSub per logical bus (there is typically only one today). The factory shape mirrors pkg/cache.Service.

func New

func New(backend Backend, redisClient redis.UniversalClient, log Logger) *Service

New creates a Service.

backend picks the implementation. When backend is BackendRedis or BackendValkey the supplied redis.UniversalClient MUST be non-nil; a nil client falls back to MemoryPubSub with a logged warning so a misconfigured HA deployment degrades gracefully. The caller owns the client lifecycle.

log is used for operational errors from the backing bus. Nil is accepted and swapped for a silent no-op.

func (*Service) NewPubSub

func (s *Service) NewPubSub(prefix string) (PubSub, error)

NewPubSub builds a PubSub for the configured backend. prefix is used only by the RESP backends to namespace channel names so multiple buses can share one Redis/Valkey keyspace without colliding; it is ignored by MemoryPubSub.

RESP paths route through newRESP so the prefix validation enforced by the public NewRedisPubSub/NewValkeyPubSub constructors applies here too; a prefix containing glob metacharacters would otherwise slip past the factory and poison SUBSCRIBE pattern matching.

type Subscription

type Subscription interface {
	// C returns the delivery channel. It is closed by Close and by
	// Close on the owning PubSub; readers should treat a closed
	// channel as "subscription ended" rather than an error.
	C() <-chan []byte

	// Close cancels the subscription and closes C. Idempotent.
	Close() error
}

Subscription is a single subscriber's handle to a topic. Each Subscribe call returns a fresh Subscription with its own channel; Close is the only path to release it.

Jump to

Keyboard shortcuts

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