snapshot of https://github.com/centrifugal/centrifugo.git (master, 2026-09-16; blobs > 131072B omitted)
This commit is contained in:
217
internal/pgstreambroker/publish.go
Normal file
217
internal/pgstreambroker/publish.go
Normal file
@@ -0,0 +1,217 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user