Files
upstream-centrifugo/internal/pgstreambroker/publish.go
upstream-mirror b352a9a30d
Some checks failed
test / Lint (push) Failing after 3s
test / Test with Go 1.26.8 (push) Failing after 2s
snapshot of https://github.com/centrifugal/centrifugo.git (master, 2026-09-16; blobs > 131072B omitted)
2026-09-16 01:17:21 +05:00

218 lines
6.4 KiB
Go

package pgstreambroker
import (
"context"
"encoding/json"
"fmt"
"github.com/centrifugal/centrifuge"
)
// Publish writes a publication to the history table and assigns a stream
// position. The publish SQL function does the heavy lifting (atomic shard
// lock + insert-or-lock meta + version/idempotency checks + offset increment
// + history INSERT + NOTIFY) — this Go-side method just marshals options,
// converts Go zero values to NULL pointers where the SQL function uses
// COALESCE, and reshapes the result.
// Publish is the centrifuge.Broker Publish implementation. Publish count and
// latency metrics are provided by the centrifuge library at the Node level —
// the PG broker only tracks PG-specific operational metrics (cursor lag,
// partition count, cleanup rows, orphan rows).
func (e *PostgresStreamBroker) Publish(ch string, data []byte, opts centrifuge.PublishOptions) (centrifuge.PublishResult, error) {
ctx := context.Background()
// Marshal Tags to JSONB.
var tagsJSON any
if len(opts.Tags) > 0 {
b, err := json.Marshal(opts.Tags)
if err != nil {
return centrifuge.PublishResult{}, fmt.Errorf("postgres stream broker: marshal tags: %w", err)
}
tagsJSON = json.RawMessage(b)
}
// HistoryTTL → INTERVAL pointer (nil if 0).
var historyTTL *string
if opts.HistoryTTL > 0 {
s := durationToIntervalString(opts.HistoryTTL)
historyTTL = &s
}
// HistorySize → INTEGER pointer (nil if 0).
var historySize *int
if opts.HistorySize > 0 {
n := opts.HistorySize
historySize = &n
}
// HistoryMetaTTL with 3-tier fallback: opts → node config → StreamRetention.
// The third tier guarantees expires_at is always set, so the meta
// cleanup pass eventually fires for every channel.
historyMetaTTL := opts.HistoryMetaTTL
if historyMetaTTL == 0 {
historyMetaTTL = e.node.Config().HistoryMetaTTL
}
if historyMetaTTL == 0 {
historyMetaTTL = e.conf.StreamRetention
}
metaTTLStr := durationToIntervalString(historyMetaTTL)
metaTTL := &metaTTLStr
// Idempotency key — convert empty string to nil.
var idempotencyKey *string
var idempotencyTTL *string
if opts.IdempotencyKey != "" {
idempotencyKey = &opts.IdempotencyKey
ttl := opts.IdempotentResultTTL
if ttl == 0 {
ttl = e.conf.IdempotentResultTTL
}
s := durationToIntervalString(ttl)
idempotencyTTL = &s
}
// ClientInfo handling.
var clientID, userID *string
var connInfo, chanInfo []byte
if opts.ClientInfo != nil {
if opts.ClientInfo.ClientID != "" {
clientID = &opts.ClientInfo.ClientID
}
if opts.ClientInfo.UserID != "" {
userID = &opts.ClientInfo.UserID
}
connInfo = opts.ClientInfo.ConnInfo
chanInfo = opts.ClientInfo.ChanInfo
}
numShards := e.conf.NumShards
// Version / VersionEpoch: pointers so the SQL function sees NULL when the
// caller didn't request versioned dedup. Zero Version also maps to NULL
// so a bare publish can't accidentally clobber the stored baseline.
var versionArg *int64
var versionEpochArg *string
if opts.Version > 0 {
v := int64(opts.Version)
versionArg = &v
if opts.VersionEpoch != "" {
versionEpochArg = &opts.VersionEpoch
}
}
// Call __PREFIX__publish. Parameter order mirrors pgmapbroker's publish:
// channel, data, tags, TTLs, version/epoch, idempotency, delta flag,
// client info, num_shards.
query := fmt.Sprintf(`
SELECT out_result_id, out_channel_offset, out_epoch, out_suppressed, out_suppress_reason
FROM %s($1, $2, $3, $4::interval, $5, $6::interval, $7, $8, $9, $10::interval, $11, $12, $13, $14, $15, $16)
`, e.names.publish)
var resultID *int64
var channelOffset int64
var epoch string
var suppressed bool
var suppressReason *string
err := e.pool.QueryRow(ctx, query,
ch,
e.dataParam(data),
tagsJSON,
historyTTL,
historySize,
metaTTL,
versionArg,
versionEpochArg,
idempotencyKey,
idempotencyTTL,
opts.UseDelta,
clientID,
userID,
e.dataParam(connInfo),
e.dataParam(chanInfo),
numShards,
).Scan(&resultID, &channelOffset, &epoch, &suppressed, &suppressReason)
if err != nil {
return centrifuge.PublishResult{}, fmt.Errorf("postgres stream broker: publish: %w", err)
}
result := centrifuge.PublishResult{
StreamPosition: centrifuge.StreamPosition{
Offset: uint64(channelOffset),
Epoch: epoch,
},
}
if suppressed {
result.Suppressed = true
if suppressReason != nil {
result.SuppressReason = centrifuge.SuppressReason(*suppressReason)
}
}
// Contract quirk: for HistorySize<=0 || HistoryTTL<=0, the caller expects
// an empty StreamPosition. The SQL function still wrote a row (so it's
// delivered via the outbox) but we return zero per the Broker contract.
if opts.HistorySize <= 0 || opts.HistoryTTL <= 0 {
result.StreamPosition = centrifuge.StreamPosition{}
}
return result, nil
}
// PublishJoin publishes a join event. In fanout mode, short-circuits to the
// inner Broker (bypassing PG entirely). In non-fanout mode, inserts a kind=1
// row via the publish_join SQL function (which acquires the shard lock).
func (e *PostgresStreamBroker) PublishJoin(ch string, info *centrifuge.ClientInfo) error {
if e.conf.Broker != nil {
return e.conf.Broker.PublishJoin(ch, info)
}
return e.publishJoinViaHistory(ch, info)
}
// PublishLeave mirrors PublishJoin for leave events (kind=2).
func (e *PostgresStreamBroker) PublishLeave(ch string, info *centrifuge.ClientInfo) error {
if e.conf.Broker != nil {
return e.conf.Broker.PublishLeave(ch, info)
}
return e.publishLeaveViaHistory(ch, info)
}
func (e *PostgresStreamBroker) publishJoinViaHistory(ch string, info *centrifuge.ClientInfo) error {
return e.publishJoinLeaveViaHistory(e.names.publishJoin, ch, info)
}
func (e *PostgresStreamBroker) publishLeaveViaHistory(ch string, info *centrifuge.ClientInfo) error {
return e.publishJoinLeaveViaHistory(e.names.publishLeave, ch, info)
}
func (e *PostgresStreamBroker) publishJoinLeaveViaHistory(funcName, ch string, info *centrifuge.ClientInfo) error {
ctx := context.Background()
var clientID, userID string
var connInfo, chanInfo []byte
if info != nil {
clientID = info.ClientID
userID = info.UserID
connInfo = info.ConnInfo
chanInfo = info.ChanInfo
}
query := fmt.Sprintf(
`SELECT %s($1, $2, $3, $4, $5, $6)`,
funcName,
)
var id int64
if err := e.pool.QueryRow(ctx, query,
ch,
clientID,
userID,
e.dataParam(connInfo),
e.dataParam(chanInfo),
e.conf.NumShards,
).Scan(&id); err != nil {
return fmt.Errorf("postgres stream broker: %s: %w", funcName, err)
}
return nil
}