Documentation
¶
Index ¶
- func EnsurePublication(ctx context.Context, adminConn *pgx.Conn, publicationName string, ...) (bool, error)
- func EnsureReplicationSlot(ctx context.Context, replConn *pgconn.PgConn, adminConn *pgx.Conn, ...) (bool, error)
- func IdentifySystem(ctx context.Context, conn *pgconn.PgConn) (pglogrepl.IdentifySystemResult, error)
- func OpenAdminConnection(ctx context.Context, adminURL string) (*pgx.Conn, error)
- func OpenReplicationConnection(ctx context.Context, databaseURL string) (*pgconn.PgConn, error)
- func PublicationExists(ctx context.Context, conn *pgx.Conn, publicationName string) (bool, error)
- func SlotConfirmedFlushLSN(ctx context.Context, conn *pgx.Conn, slotName string) (pglogrepl.LSN, bool, error)
- func SlotExists(ctx context.Context, conn *pgx.Conn, slotName string) (bool, error)
- type Broadcaster
- type CDC
- type Change
- type ChangeHandler
- type ClientConfig
- type Config
- type DeliveryFunc
- type Metrics
- type MetricsProvider
- type MetricsSnapshot
- type OutboxConsumer
- type OutboxRow
- type ReplicationStream
- type Server
- func (s *Server) Handler() http.Handler
- func (s *Server) ListenAndServe(addr string) error
- func (s *Server) NewMetricsHandler(w http.ResponseWriter, r *http.Request)
- func (s *Server) NewSSEHandler(w http.ResponseWriter, r *http.Request)
- func (s *Server) Serve(ln net.Listener) error
- func (s *Server) Shutdown(ctx context.Context) error
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func EnsurePublication ¶
func EnsurePublication(ctx context.Context, adminConn *pgx.Conn, publicationName string, tables []string) (bool, error)
EnsurePublication creates the publication for the configured tables if it does not exist yet. It returns true when the publication was created, false when it already existed.
func EnsureReplicationSlot ¶
func EnsureReplicationSlot(ctx context.Context, replConn *pgconn.PgConn, adminConn *pgx.Conn, slotName string) (bool, error)
EnsureReplicationSlot creates the slot if it does not exist yet. It returns true when the slot was created, false when it already existed. Slot creation must happen on the replication connection; the existence check uses the admin connection.
func IdentifySystem ¶
func IdentifySystem(ctx context.Context, conn *pgconn.PgConn) (pglogrepl.IdentifySystemResult, error)
IdentifySystem asks the server for its identity: the system ID, the timeline ID, and the current WAL position. The WAL position is used as the starting point for replication, so the client only sees changes that happen after it starts.
func OpenAdminConnection ¶
OpenAdminConnection opens a regular pgx connection used for ordinary queries (checking and creating replication slots and publications).
func OpenReplicationConnection ¶
OpenReplicationConnection opens a dedicated connection for logical replication. The connection URL must include `replication=database`; the returned connection is used for IDENTIFY_SYSTEM, START_REPLICATION and the streaming of changes.
func PublicationExists ¶
PublicationExists reports whether a publication with the given name already exists in the database.
func SlotConfirmedFlushLSN ¶
func SlotConfirmedFlushLSN(ctx context.Context, conn *pgx.Conn, slotName string) (pglogrepl.LSN, bool, error)
SlotConfirmedFlushLSN returns the slot's confirmed flush position — the point up to which a previous run of the client consumed the WAL. The second return value is false when the slot has no usable saved position yet (a freshly created slot, or one whose position is 0/0).
Types ¶
type Broadcaster ¶
type Broadcaster struct {
// contains filtered or unexported fields
}
func NewBroadcaster ¶
func NewBroadcaster() *Broadcaster
func (*Broadcaster) ChangesDropped ¶
func (b *Broadcaster) ChangesDropped() int64
ChangesDropped returns the total number of changes dropped for active subscribers because their buffers were full.
func (*Broadcaster) Publish ¶
func (b *Broadcaster) Publish(change *Change)
func (*Broadcaster) Subscribe ¶
func (b *Broadcaster) Subscribe(id string, bufferSize int) chan *Change
func (*Broadcaster) SubscriberCount ¶
func (b *Broadcaster) SubscriberCount() int
SubscriberCount returns the number of active subscribers.
func (*Broadcaster) Unsubscribe ¶
func (b *Broadcaster) Unsubscribe(id string)
type CDC ¶
type CDC struct {
// contains filtered or unexported fields
}
CDC is a change data capture client wrapping the phylax building blocks.
func New ¶
New validates cfg and returns a CDC client. It does not connect; the connections are established by Start.
func (*CDC) Broadcaster ¶
func (c *CDC) Broadcaster() *Broadcaster
Broadcaster returns the client's change broadcaster, shared by every OnChange registration. Use it to plug the client into a phylax.Server so SSE clients receive the same changes as OnChange subscribers.
func (*CDC) MetricsSnapshot ¶
func (c *CDC) MetricsSnapshot() MetricsSnapshot
MetricsSnapshot returns a point-in-time reading of the live metrics: changes processed by the current stream, changes dropped for the broadcaster's subscribers, the active subscriber count, and the current replication lag. It reads only in-memory state — no database or network access. With no stream running yet, processed and lag are zero.
func (*CDC) OnChange ¶
OnChange registers fn to be called for every decoded change. Each registration runs in its own goroutine; the goroutine exits when Start shuts down and unsubscribes the registration.
func (*CDC) OnOutboxDelivery ¶ added in v0.3.0
OnOutboxDelivery registers the handler called for every outbox row (inserts on Config.OutboxTable). The handler must be idempotent — phylax delivers at-least-once, so the same row id may arrive more than once. Without a handler, phylax logs each row and acks it.
func (*CDC) Server ¶
Server returns a phylax.Server wired to this client: /events fans out the client's changes, /metrics/stream reports the live metrics, and /dashboard serves the embedded Phylax Console.
func (*CDC) Start ¶
Start connects, ensures the slot and publication exist, and runs the read/decode/broadcast loop until ctx is cancelled.
If the replication connection drops mid-stream, Start reconnects with exponential backoff (1s doubling up to 30s) and resumes from the slot's confirmed position, logging each retry. It keeps retrying — even when the database is unreachable at startup — until ctx is cancelled.
Only transient failures (connection loss, server restart) are retried. Permanent failures — e.g. unknown tables or bad credentials — stop Start immediately and are returned as errors.
On ctx cancellation Start shuts down gracefully: the connections are closed and every OnChange registration is unsubscribed, so their goroutines exit via the closed subscriber channel. A clean shutdown returns nil.
type Change ¶
type Change struct {
// Table is the name of the table the change happened on.
Table string
// Operation is one of "insert", "update", "delete" or "truncate".
// A truncate change means every row in Table was removed: it carries no
// row data (OldRow and NewRow are nil), and TRUNCATE a, b, c emits one
// truncate change per table.
Operation string
// OldRow holds the pre-change column values (nil for inserts and
// truncates).
OldRow map[string]any
// NewRow holds the post-change column values (nil for deletes and
// truncates).
NewRow map[string]any
}
Change describes one logical change on a single row — or, for a TRUNCATE, on a whole table.
func Decode ¶
func Decode(walData []byte, relations map[uint32]*pglogrepl.RelationMessage, metrics *Metrics) ([]*Change, error)
Decode parses one chunk of WAL data and returns the Changes it describes. It returns an empty slice when the message carries no row change (relation metadata, transaction begin/commit, keepalives, ...). Most messages yield at most one Change; a TRUNCATE statement can truncate several tables at once, so it yields one Change per truncated table.
Every successfully decoded Change increments metrics.ChangesProcessed, if metrics is non-nil.
Relation metadata is cached in `relations` and reused across calls: the server sends a RelationMessage the first time a table is referenced, and afterwards row data is compact — tuples reference columns by index and rely on the cached relation for the column names.
type ChangeHandler ¶
ChangeHandler receives every decoded change. Returning an error aborts the replication stream.
type ClientConfig ¶
type ClientConfig struct {
// DatabaseURL is a libpq connection string for the *replication*
// connection. Logical replication requires the special parameter
// `replication=database`; pgx only allows it on a single dedicated
// connection, not a pool.
DatabaseURL string
// AdminURL is a normal connection string used for administrative
// queries such as checking and creating slots and publications.
AdminURL string
// SlotName is the name of the logical replication slot.
SlotName string
// PublicationName is the publication whose changes we subscribe to.
// The pgoutput plugin is told this name when replication starts.
PublicationName string
// Tables lists the tables the publication is created for. It is only
// used the first time the client runs, when the publication does not
// exist yet.
Tables []string
// HeartbeatInterval controls how often the client sends a standby
// status update back to the server. These updates tell the server how
// far the client has consumed the WAL; without them the connection is
// dropped after wal_sender_timeout and the slot never advances.
HeartbeatInterval time.Duration
}
ClientConfig bundles every runtime setting the client needs. It is the per-connection configuration used internally by the replication stream; the public CDC wrapper (see cdc.go) exposes a simpler Config on top.
func DefaultClientConfig ¶
func DefaultClientConfig() ClientConfig
DefaultClientConfig returns an example configuration for local development. The connection strings are placeholders — replace them with your own DSNs; the other values are the phylax defaults (slot my_slot, publication my_publication).
type Config ¶
type Config struct {
// DSN is a libpq connection string. It is used for the admin connection
// as-is, and for the replication connection with `replication=database`
// appended.
DSN string
// Tables lists the tables the publication is created for. It is only
// used the first time the client runs, when the publication does not
// exist yet.
Tables []string
// SlotName is the name of the logical replication slot. Defaults to
// "my_slot" when empty.
SlotName string
// PublicationName is the publication whose changes are replicated.
// Defaults to "my_publication" when empty.
PublicationName string
// ChangeBufferSize is the per-subscriber channel buffer used by
// OnChange. A slow consumer with a full buffer drops changes
// (counted in MetricsSnapshot.ChangesDropped) rather than stalling
// the stream, so size this for your biggest burst. Defaults to 100
// when <= 0. Each buffered change costs roughly a kilobyte.
ChangeBufferSize int
OutboxTable string
}
Config configures a CDC client.
type DeliveryFunc ¶ added in v0.3.0
DeliveryFunc is the user-supplied handler called once per outbox row. Returning nil marks the row delivered; returning an error triggers retry.
DELIVERY MUST BE IDEMPOTENT. phylax delivers at-least-once: on restart it resumes from the slot's saved position and replays every outbox insert, including rows already acked with delivered_at, and in-flight retry state is lost on crash. The same row may therefore be delivered more than once — design the handler (and the broker it talks to) to tolerate duplicate deliveries of the same row ID.
type Metrics ¶
type Metrics struct {
// ChangesProcessed counts every successfully decoded, non-nil Change.
ChangesProcessed atomic.Int64
}
Metrics holds the counters the metrics stream reports. It is owned by a ReplicationStream and updated by the decode path.
type MetricsProvider ¶
type MetricsProvider interface {
MetricsSnapshot() MetricsSnapshot
}
MetricsProvider supplies the live metrics snapshot. It is implemented by the CDC client, which owns the current stream and the change broadcaster.
type MetricsSnapshot ¶
type MetricsSnapshot struct {
ChangesProcessed int64 `json:"changes_processed"`
ChangesDropped int64 `json:"changes_dropped"`
Subscribers int `json:"subscribers"`
ReplicationLag uint64 `json:"replication_lag_bytes"`
OutboxDelivered int64 `json:"outbox_delivered"`
OutboxInflight int64 `json:"outbox_inflight"`
OutboxFailed int64 `json:"outbox_failed"`
}
MetricsSnapshot is a point-in-time reading of the live metrics.
type OutboxConsumer ¶ added in v0.3.0
type OutboxConsumer struct {
// contains filtered or unexported fields
}
OutboxConsumer reads outbox-table inserts off the WAL stream, delivers them via a user-supplied DeliveryFunc, retries failures with backoff, and acks (marks delivered) on success.
Delivery is asynchronous and bounded: each row is dispatched to a per-topic drainer goroutine, so a slow or down broker never blocks WAL consumption. Within a topic, rows are delivered strictly in order; across topics, delivery runs in parallel. A global semaphore caps the number of concurrent drainers so a burst of distinct topics can't spin up unbounded goroutines.
func NewOutboxConsumer ¶ added in v0.3.0
func NewOutboxConsumer(db *pgx.Conn, deliver DeliveryFunc, tableName string) *OutboxConsumer
NewOutboxConsumer wires up a consumer against the given connection and handler. tableName is the table whose inserts are treated as outbox events.
func (*OutboxConsumer) Handle ¶ added in v0.3.0
func (oc *OutboxConsumer) Handle(ctx context.Context, c *Change) bool
Handle is the entrypoint the stream calls for every decoded Change. It returns handled=true if the change belonged to the outbox (regardless of delivery success) — the caller should skip broadcaster fan-out for handled changes.
func (*OutboxConsumer) Stats ¶ added in v0.3.1
func (oc *OutboxConsumer) Stats() (delivered, inflight, failed int64)
Stats returns the outbox consumer's live counters for the metrics stream: cumulative delivered, currently in-flight, and cumulative failed (rows that exhausted retries and were left pending).
type OutboxRow ¶ added in v0.3.0
OutboxRow is a single pending outbox row decoded from a WAL Change.
func ToOutboxRow ¶ added in v0.3.0
ToOutboxRow converts a decoded Change into an OutboxRow if it is an insert on the outbox table. ok is false if the change isn't relevant (wrong table/operation) — that's not an error, just "not for you." A malformed but relevant row reports ok=true with a non-nil err.
type ReplicationStream ¶
type ReplicationStream struct {
// contains filtered or unexported fields
}
ReplicationStream consumes messages from a running replication session.
func NewReplicationStream ¶
func NewReplicationStream(ctx context.Context, conn *pgconn.PgConn, cfg ClientConfig, startLSN pglogrepl.LSN, logger *slog.Logger, handle ChangeHandler, db *pgx.Conn, delivery DeliveryFunc, outboxTable string) (*ReplicationStream, error)
NewReplicationStream issues START_REPLICATION for the configured slot and returns a stream ready to consume. The start LSN comes from IDENTIFY_SYSTEM; the server resumes from there. The publication name is passed to the pgoutput plugin so it knows which tables to send.
func (*ReplicationStream) Broadcaster ¶
func (s *ReplicationStream) Broadcaster() *Broadcaster
Broadcaster returns the stream's fan-out broadcaster. Every decoded change is published to it, so subscribers receive each change as it streams in.
func (*ReplicationStream) OutboxStats ¶ added in v0.3.1
func (s *ReplicationStream) OutboxStats() (delivered, inflight, failed int64)
OutboxStats returns the outbox consumer's live counters (0 when outbox is disabled). Read by CDC.MetricsSnapshot for the dashboard.
func (*ReplicationStream) ReplicationLag ¶
func (s *ReplicationStream) ReplicationLag() uint64
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
func NewServer ¶
func NewServer(broadcaster *Broadcaster, metrics MetricsProvider) *Server
NewServer returns a Server that fans decoded changes out through the given broadcaster — typically the CDC client's broadcaster, so SSE clients see every change OnChange subscribers see. metrics supplies the live counters for the /metrics/stream endpoint; a nil provider reports an all-zero snapshot.
func (*Server) Handler ¶
Handler returns an http.Handler serving /events, /metrics/stream, and /dashboard on a single mux.
func (*Server) ListenAndServe ¶
ListenAndServe serves both endpoints on addr until Shutdown is called.
func (*Server) NewMetricsHandler ¶
func (s *Server) NewMetricsHandler(w http.ResponseWriter, r *http.Request)
NewMetricsHandler streams a JSON metrics snapshot once per tick as one SSE `data: ...` event, exiting when the client disconnects. It reads only in-memory counters — it does not subscribe to the change broadcaster and never touches Postgres.
func (*Server) NewSSEHandler ¶
func (s *Server) NewSSEHandler(w http.ResponseWriter, r *http.Request)
NewSSEHandler streams every change the broadcaster fans out as one SSE `data: ...` event per change. It exits when the client disconnects.
func (*Server) Serve ¶
Serve serves both endpoints on ln until Shutdown is called. It returns http.ErrServerClosed after a graceful shutdown.
func (*Server) Shutdown ¶
Shutdown gracefully stops the HTTP server started by Serve or ListenAndServe: it waits for in-flight requests to finish or ctx to expire. It is a no-op when the server is not running.
Shutdown also works when it is called before Serve has started: it still marks the server shut down, so a Serve that starts afterwards refuses its listener and returns http.ErrServerClosed instead of serving forever without ever being stoppable.
