Documentation
¶
Overview ¶
Package cluster implements the coordination of the popgres app instances (nodes) that share the same Postgres database.
The nodes exchange short messages with LISTEN/NOTIFY. All messages are sent on a single Postgres channel (Channel) and are routed to the subscribers by their topic, so that new topics don't require a new LISTEN.
The NOTIFY payloads are limited to 8000 bytes by Postgres, so the messages are expected to carry only ids (the larger data should be stored in a table).
Index ¶
- Constants
- func LockKey(name string) int64
- func SlotName(database string) string
- func XactLock(db dbx.Builder, name string) error
- type Cluster
- func (c *Cluster) AddRealtimeFailure()
- func (c *Cluster) CheckReplicationSupport(ctx context.Context) error
- func (c *Cluster) ClaimCronRun(jobId string, slot time.Time) (bool, error)
- func (c *Cluster) DeleteExpired() error
- func (c *Cluster) DroppedMessages() int64
- func (c *Cluster) Enabled() bool
- func (c *Cluster) FindRealtimeEvents(ids []string) ([]*RealtimeEvent, error)
- func (c *Cluster) IsLocked(ctx context.Context, name string) (bool, error)
- func (c *Cluster) IsReplicationLeader() bool
- func (c *Cluster) KV() *KV
- func (c *Cluster) NodeId() string
- func (c *Cluster) Notify(topic string, payload string) error
- func (c *Cluster) OnReconnect(fn func()) (unsubscribe func())
- func (c *Cluster) Publish(db dbx.Builder, topic string, payload string) error
- func (c *Cluster) RateLimitHit(key string, maxAllowed int, interval time.Duration) (bool, error)
- func (c *Cluster) Ready() <-chan struct{}
- func (c *Cluster) RealtimeFailures() int64
- func (c *Cluster) RealtimeSource() string
- func (c *Cluster) SetRealtimeSource(source string)
- func (c *Cluster) SlotMetrics(ctx context.Context) (*SlotMetrics, error)
- func (c *Cluster) Start()
- func (c *Cluster) StartReplication(config ReplicationConfig)
- func (c *Cluster) Stop()
- func (c *Cluster) StopReplication()
- func (c *Cluster) Subscribe(topic string, handler Handler) (unsubscribe func())
- func (c *Cluster) TryLock(ctx context.Context, name string) (unlock func(), err error)
- type Config
- type EventsDeduplicator
- type Handler
- type KV
- func (kv *KV) Claim(key string, value string, ttl time.Duration) (bool, error)
- func (kv *KV) Delete(key string) error
- func (kv *KV) DeleteExpired() error
- func (kv *KV) Get(key string) (string, bool, error)
- func (kv *KV) Has(key string) bool
- func (kv *KV) Pop(key string) (string, bool, error)
- func (kv *KV) Set(key string, value string, ttl time.Duration) error
- type Message
- type RealtimeEvent
- type ReplicationConfig
- type SlotMetrics
Constants ¶
const ( RealtimeSourceHooks = "hooks" RealtimeSourceReplication = "replication" )
The realtime event sources.
const ( // PublicationName is the Postgres publication with the collection tables. PublicationName = "popgres_rt_pub" // TopicRealtimeEvents is the cluster topic with the ids of the stored realtime events // (comma separated). TopicRealtimeEvents = "rt_events" // RealtimeEventsTable is the UNLOGGED table of the realtime events (created by the system migrations). RealtimeEventsTable = "_realtime_events" // RetainedWALWarningSize is the retained WAL size (in bytes) of the // realtime slot above which a warning is logged. RetainedWALWarningSize = 1 << 30 )
const Channel = "popgres_cluster"
Channel is the Postgres LISTEN/NOTIFY channel used by the cluster messages.
const CronRunsTable = "_cron_runs"
CronRunsTable is the UNLOGGED table of the claimed cron job executions (created by the system migrations).
const KVTable = "_kv_ttl"
KVTable is the UNLOGGED table of the shared keys (created by the system migrations).
const MaxPayloadSize = 7000
MaxPayloadSize is the max size in bytes of a message payload (the Postgres NOTIFY limit minus the message header).
const RateLimitsTable = "_rate_limits"
RateLimitsTable is the UNLOGGED table of the shared rate limit counters (created by the system migrations).
const ( // RealtimeEventsMaxAge is the max age of the published realtime events. RealtimeEventsMaxAge = 5 * time.Minute )
Variables ¶
This section is empty.
Functions ¶
func LockKey ¶
LockKey returns the Postgres advisory lock key of the named lock.
The advisory locks are per database, so the same name can be used by multiple popgres databases on the same server.
func SlotName ¶
SlotName returns the name of the realtime logical replication slot of the database.
The name is "popgres_rt_<database>" (with the invalid characters replaced) and it is truncated with a hash suffix if it is longer than 63 characters.
Types ¶
type Cluster ¶
type Cluster struct {
// contains filtered or unexported fields
}
Cluster manages the LISTEN connection of a node and the dispatch of the received messages to the topic subscribers.
The LISTEN connection is dedicated (it is not part of the app pool) and it is automatically reconnected with a backoff in case of an error.
func (*Cluster) AddRealtimeFailure ¶
func (c *Cluster) AddRealtimeFailure()
AddRealtimeFailure increments the counter of the realtime replication events that couldn't be broadcasted (see Cluster.RealtimeFailures).
func (*Cluster) CheckReplicationSupport ¶
CheckReplicationSupport reports whether the logical replication can be used for the realtime events (wal_level=logical and a role with the REPLICATION attribute).
func (*Cluster) ClaimCronRun ¶
ClaimCronRun claims the execution of a cron job for the specified scheduled slot and reports whether the current node should run it.
Only one node can claim the same job and slot.
func (*Cluster) DeleteExpired ¶
DeleteExpired deletes the expired rows of the cluster tables (the old cron runs, the expired keys and rate limits, and the old realtime events).
func (*Cluster) DroppedMessages ¶
DroppedMessages returns the number of the received messages that were dropped because the dispatch queue was full.
func (*Cluster) Enabled ¶
Enabled reports whether the cluster mode is enabled.
The state synchronization of the nodes (eg. the cached collections and the realtime events) works always, while the features with an extra cost per request (eg. the shared rate limits) are used only in cluster mode.
func (*Cluster) FindRealtimeEvents ¶
func (c *Cluster) FindRealtimeEvents(ids []string) ([]*RealtimeEvent, error)
FindRealtimeEvents returns the stored realtime events with the specified ids (ordered by id).
func (*Cluster) IsLocked ¶
IsLocked reports whether the named session level advisory lock is held by a session.
func (*Cluster) IsReplicationLeader ¶
IsReplicationLeader reports whether the current node consumes the replication slot.
func (*Cluster) Notify ¶
Notify is similar to Cluster.Publish but the message is sent with the main database pool (it is not part of the current transaction, if any).
func (*Cluster) OnReconnect ¶
func (c *Cluster) OnReconnect(fn func()) (unsubscribe func())
OnReconnect registers a function that is called after the LISTEN connection was reestablished (the messages published in the meantime are lost, so the subscribers should resync their state) and returns a function that removes it.
func (*Cluster) Publish ¶
Publish sends a message to all nodes (including the current one).
The message is sent with pg_notify using the provided db builder. If the builder is a transaction, the message is delivered only after its commit.
func (*Cluster) RateLimitHit ¶
RateLimitHit registers a hit of the specified rate limit key and reports whether it is allowed (aka. the total hits in the current window are <= maxAllowed).
It implements the same fixed window strategy as the in-memory app rate limiter: the window starts with the first hit and resets after interval.
func (*Cluster) Ready ¶
func (c *Cluster) Ready() <-chan struct{}
Ready returns a channel that is closed after the first successful LISTEN.
func (*Cluster) RealtimeFailures ¶
RealtimeFailures returns the number of the realtime replication events that couldn't be broadcasted to the local clients.
func (*Cluster) RealtimeSource ¶
RealtimeSource returns the realtime source of the current node (RealtimeSourceHooks if not set).
func (*Cluster) SetRealtimeSource ¶
SetRealtimeSource sets the realtime source of the current node (see RealtimeSourceHooks).
func (*Cluster) SlotMetrics ¶
func (c *Cluster) SlotMetrics(ctx context.Context) (*SlotMetrics, error)
SlotMetrics returns the realtime replication slot metrics of the database (or nil if there is no slot).
func (*Cluster) Start ¶
func (c *Cluster) Start()
Start starts listening for the cluster messages in the background.
It is no-op if the cluster is already started.
func (*Cluster) StartReplication ¶
func (c *Cluster) StartReplication(config ReplicationConfig)
StartReplication starts competing for the replication leadership in the background.
It is no-op if it is already started.
func (*Cluster) Stop ¶
func (c *Cluster) Stop()
Stop closes the LISTEN connection and waits for the background goroutines to finish.
It is no-op if the cluster is not started.
func (*Cluster) StopReplication ¶
func (c *Cluster) StopReplication()
StopReplication stops the replication (releasing the leadership) and waits for it to finish.
func (*Cluster) Subscribe ¶
Subscribe registers a handler for the messages of the specified topic and returns a function that removes it.
The handlers are called sequentially from a single dispatch goroutine, so they should not block for long. The messages published by the current node are delivered too (check Message.NodeId to skip them).
func (*Cluster) TryLock ¶
TryLock tries to acquire the named session level advisory lock without waiting.
On success it returns a function that releases the lock. The lock is held by a dedicated connection of the main pool until it is released (or until the connection is closed, eg. if the app instance crashes).
It returns nil (and no error) if the lock is held by another session.
type Config ¶
type Config struct {
// DSN returns the connection string of the shared database
// (used for the dedicated LISTEN connection).
DSN func() string
// DB returns the main database pool (not a transaction).
DB func() dbx.Builder
// Logger returns the logger to use.
Logger func() *slog.Logger
// Enabled reports whether the cluster mode is enabled
// (see [Cluster.Enabled]).
Enabled func() bool
}
Config defines the Cluster dependencies.
They are functions because they could change on app bootstrap.
type EventsDeduplicator ¶
type EventsDeduplicator struct {
// contains filtered or unexported fields
}
EventsDeduplicator remembers the recently received event ids so that the events delivered more than once (eg. after a leader change) are discarded.
func NewEventsDeduplicator ¶
func NewEventsDeduplicator(max int) *EventsDeduplicator
NewEventsDeduplicator creates a new deduplicator that remembers the last max ids.
func (*EventsDeduplicator) Filter ¶
func (d *EventsDeduplicator) Filter(ids []string) []string
Filter returns the ids that were not seen before (sorted) and remembers them.
func (*EventsDeduplicator) Forget ¶
func (d *EventsDeduplicator) Forget(ids []string)
Forget removes the provided ids from the received ones (eg. the events that failed to broadcast, so that a new publish delivers them).
type KV ¶
type KV struct {
// contains filtered or unexported fields
}
KV is a key-value store with expiration that is shared by all nodes.
It replaces the app in-memory store for the temporary state that must be visible to all nodes (eg. the resend timeouts of the auth emails).
The queries are executed with the main database pool (and not with the current transaction, if any) because the state is not part of the transaction data and it is usually written after the transaction completion.
func (*KV) Claim ¶
Claim stores the value of the key for the ttl duration only if the key doesn't exist or has expired.
It reports whether the key was stored (aka. claimed by the caller).
func (*KV) DeleteExpired ¶
DeleteExpired deletes all expired keys.
func (*KV) Get ¶
Get returns the value of the key and whether it exists (the expired keys don't exist).
type Message ¶
type Message struct {
// NodeId is the id of the node that published the message.
NodeId string
// Topic is the message topic (eg. "collections").
Topic string
// Payload is the optional message data.
Payload string
}
Message is a cluster message received from a node (including the current one).
type RealtimeEvent ¶
type RealtimeEvent struct {
Id string `db:"id"`
Source string `db:"source"`
Op string `db:"op"`
CollectionId string `db:"collection_id"`
RecordId string `db:"record_id"`
Snapshot string `db:"snapshot"` // json, only for the deleted records
}
RealtimeEvent is a stored realtime event.
type ReplicationConfig ¶
type ReplicationConfig struct {
// PrepareTables ensures that the publication contains all collection
// tables (with REPLICA IDENTITY FULL). It is called by the leader before
// consuming the slot.
PrepareTables func() error
// ResolveCollection returns the collection id of the specified table
// (or the table name if it can't be resolved).
ResolveCollection func(table string) string
}
ReplicationConfig defines the callbacks used by the replication leader.
type SlotMetrics ¶
type SlotMetrics struct {
Slot string `db:"slot_name" json:"slot"`
// Active reports whether a node consumes the slot.
Active bool `db:"active" json:"active"`
// WALStatus is the Postgres wal_status of the slot (eg. "reserved" or "lost").
WALStatus string `db:"wal_status" json:"walStatus"`
// LagBytes is the size of the WAL that is not confirmed by the consumer yet.
LagBytes int64 `db:"lag_bytes" json:"lagBytes"`
// RetainedBytes is the size of the WAL retained by the slot.
RetainedBytes int64 `db:"retained_bytes" json:"retainedBytes"`
}
SlotMetrics are the realtime replication slot metrics.