114 lines
3.4 KiB
Go
114 lines
3.4 KiB
Go
package pgstreambroker
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/centrifugal/centrifugo/v6/internal/metrics"
|
|
"github.com/centrifugal/centrifugo/v6/internal/pgoutbox"
|
|
)
|
|
|
|
// isShutdownErr reports whether err is a normal-shutdown signal — context
|
|
// cancellation surfacing because the broker is being closed mid-tick. Those
|
|
// aren't real failures and shouldn't be logged at error level, since they
|
|
// would otherwise pollute CI logs and mask genuine cleanup errors.
|
|
func isShutdownErr(err error) bool {
|
|
return errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)
|
|
}
|
|
|
|
// newPartitioner constructs a pgoutbox.Partitioner configured for the
|
|
// stream broker's history table.
|
|
func (e *PostgresStreamBroker) newPartitioner() *pgoutbox.Partitioner {
|
|
return &pgoutbox.Partitioner{
|
|
Pool: e.pool,
|
|
ParentTable: e.names.stream,
|
|
CleanupInterval: e.conf.CleanupInterval,
|
|
LookaheadDays: e.conf.PartitionLookaheadDays,
|
|
RetentionDays: e.conf.PartitionRetentionDays,
|
|
ErrorFn: e.logErrorMsg,
|
|
}
|
|
}
|
|
|
|
// runPartitionWorker manages partition lookahead creation and retention drops.
|
|
func (e *PostgresStreamBroker) runPartitionWorker() {
|
|
p := e.newPartitioner()
|
|
p.Run(e.cancelCtx, e.closeCh)
|
|
}
|
|
|
|
// runCleanupWorker runs the cleanup pass on CleanupInterval ticks.
|
|
func (e *PostgresStreamBroker) runCleanupWorker() {
|
|
ticker := time.NewTicker(e.conf.CleanupInterval)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-e.cancelCtx.Done():
|
|
return
|
|
case <-e.closeCh:
|
|
return
|
|
case <-ticker.C:
|
|
e.cleanupSupportTables(e.cancelCtx)
|
|
if e.sampler != nil {
|
|
e.sampler.sample(e.cancelCtx)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func addCleanupRows(broker, pass string, n int64) {
|
|
if metrics.BrokerPostgresCleanupRemovedTotal != nil {
|
|
metrics.BrokerPostgresCleanupRemovedTotal.WithLabelValues(broker, pass).Add(float64(n))
|
|
}
|
|
}
|
|
|
|
// cleanupSupportTables deletes expired meta and idempotency rows in bounded
|
|
// batches to avoid long-running DELETE locks on busy tables. Each batch is
|
|
// capped at cleanupBatchSize rows; passes repeat with a cleanupChunkPause gap
|
|
// until fewer than a full batch is deleted. Context cancellation (broker
|
|
// shutting down mid-tick) is treated as normal and not logged.
|
|
func (e *PostgresStreamBroker) cleanupSupportTables(ctx context.Context) {
|
|
e.cleanupExpiredBatched(ctx, "meta", fmt.Sprintf(
|
|
`DELETE FROM %[1]s WHERE channel IN (
|
|
SELECT channel FROM %[1]s
|
|
WHERE expires_at IS NOT NULL AND expires_at < NOW()
|
|
LIMIT $1
|
|
)`, e.names.meta))
|
|
e.cleanupExpiredBatched(ctx, "idempotency", fmt.Sprintf(
|
|
`DELETE FROM %[1]s WHERE (channel, idempotency_key) IN (
|
|
SELECT channel, idempotency_key FROM %[1]s
|
|
WHERE expires_at < NOW()
|
|
LIMIT $1
|
|
)`, e.names.idempotency))
|
|
}
|
|
|
|
const (
|
|
cleanupBatchSize = 1000
|
|
cleanupChunkPause = 100 * time.Millisecond
|
|
)
|
|
|
|
// cleanupExpiredBatched runs a chunked DELETE loop for the given table.
|
|
// Stops when a batch deletes fewer rows than cleanupBatchSize or ctx is done.
|
|
func (e *PostgresStreamBroker) cleanupExpiredBatched(ctx context.Context, passName, query string) {
|
|
for {
|
|
res, err := e.pool.Exec(ctx, query, cleanupBatchSize)
|
|
if err != nil {
|
|
if !isShutdownErr(err) {
|
|
e.logErrorMsg("error cleaning up expired "+passName, err)
|
|
}
|
|
return
|
|
}
|
|
addCleanupRows(e.conf.Name, passName, res.RowsAffected())
|
|
if res.RowsAffected() < cleanupBatchSize {
|
|
return
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-e.closeCh:
|
|
return
|
|
case <-time.After(cleanupChunkPause):
|
|
}
|
|
}
|
|
}
|