156 lines
3.0 KiB
Go
156 lines
3.0 KiB
Go
|
|
package realtime
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"encoding/json"
|
||
|
|
"strings"
|
||
|
|
"sync"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/jackc/pgx/v5"
|
||
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
||
|
|
"github.com/zeromicro/go-zero/core/logx"
|
||
|
|
)
|
||
|
|
|
||
|
|
const (
|
||
|
|
defaultOutboxInterval = time.Second
|
||
|
|
defaultOutboxBatch = 100
|
||
|
|
)
|
||
|
|
|
||
|
|
type OutboxOptions struct {
|
||
|
|
DataSource string
|
||
|
|
IntervalMillis int
|
||
|
|
BatchSize int
|
||
|
|
}
|
||
|
|
|
||
|
|
type OutboxPublisher struct {
|
||
|
|
pool *pgxpool.Pool
|
||
|
|
bus Bus
|
||
|
|
interval time.Duration
|
||
|
|
batch int
|
||
|
|
ctx context.Context
|
||
|
|
cancel context.CancelFunc
|
||
|
|
wg sync.WaitGroup
|
||
|
|
}
|
||
|
|
|
||
|
|
func NewOutboxPublisher(ctx context.Context, opts OutboxOptions, bus Bus) (*OutboxPublisher, error) {
|
||
|
|
dataSource := strings.TrimSpace(opts.DataSource)
|
||
|
|
if dataSource == "" || bus == nil {
|
||
|
|
return nil, nil
|
||
|
|
}
|
||
|
|
pool, err := pgxpool.New(ctx, dataSource)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
interval := time.Duration(opts.IntervalMillis) * time.Millisecond
|
||
|
|
if interval <= 0 {
|
||
|
|
interval = defaultOutboxInterval
|
||
|
|
}
|
||
|
|
batch := opts.BatchSize
|
||
|
|
if batch <= 0 {
|
||
|
|
batch = defaultOutboxBatch
|
||
|
|
}
|
||
|
|
runCtx, cancel := context.WithCancel(context.Background())
|
||
|
|
return &OutboxPublisher{
|
||
|
|
pool: pool,
|
||
|
|
bus: bus,
|
||
|
|
interval: interval,
|
||
|
|
batch: batch,
|
||
|
|
ctx: runCtx,
|
||
|
|
cancel: cancel,
|
||
|
|
}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (p *OutboxPublisher) Start() error {
|
||
|
|
if p == nil || p.pool == nil {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
p.wg.Add(1)
|
||
|
|
go func() {
|
||
|
|
defer p.wg.Done()
|
||
|
|
p.loop()
|
||
|
|
}()
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (p *OutboxPublisher) Run() error {
|
||
|
|
if p == nil || p.pool == nil {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
p.loop()
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (p *OutboxPublisher) Shutdown() {
|
||
|
|
if p == nil {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if p.cancel != nil {
|
||
|
|
p.cancel()
|
||
|
|
}
|
||
|
|
p.wg.Wait()
|
||
|
|
if p.pool != nil {
|
||
|
|
p.pool.Close()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (p *OutboxPublisher) loop() {
|
||
|
|
ticker := time.NewTicker(p.interval)
|
||
|
|
defer ticker.Stop()
|
||
|
|
for {
|
||
|
|
if err := p.publishPending(p.ctx); err != nil && p.ctx.Err() == nil {
|
||
|
|
logx.Errorf("publish outbox events failed: %v", err)
|
||
|
|
}
|
||
|
|
select {
|
||
|
|
case <-p.ctx.Done():
|
||
|
|
return
|
||
|
|
case <-ticker.C:
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (p *OutboxPublisher) publishPending(ctx context.Context) error {
|
||
|
|
tx, err := p.pool.Begin(ctx)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
defer tx.Rollback(ctx)
|
||
|
|
|
||
|
|
rows, err := tx.Query(ctx, `
|
||
|
|
SELECT id, user_id, aggregate_id, event_type, thread_id, payload_json, created_at
|
||
|
|
FROM outbox_events
|
||
|
|
WHERE status = 'pending'
|
||
|
|
ORDER BY created_at ASC
|
||
|
|
LIMIT $1
|
||
|
|
FOR UPDATE SKIP LOCKED
|
||
|
|
`, p.batch)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
events, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (Event, error) {
|
||
|
|
var event Event
|
||
|
|
var payload []byte
|
||
|
|
if err := row.Scan(&event.ID, &event.UserID, &event.ProjectID, &event.Type, &event.ThreadID, &payload, &event.CreatedAt); err != nil {
|
||
|
|
return Event{}, err
|
||
|
|
}
|
||
|
|
event.Payload = json.RawMessage(payload)
|
||
|
|
return event, nil
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
for _, event := range events {
|
||
|
|
if err := p.bus.Publish(ctx, event); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if _, err := tx.Exec(ctx, `
|
||
|
|
UPDATE outbox_events
|
||
|
|
SET status = 'published', published_at = NOW()
|
||
|
|
WHERE id = $1
|
||
|
|
`, event.ID); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return tx.Commit(ctx)
|
||
|
|
}
|