outbox

package
v1.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 14, 2026 License: MIT Imports: 9 Imported by: 0

Documentation

Overview

Package outbox implements the transactional-outbox pattern: events are written into an outbox table within the same database transaction as the business data, and a background Relay publishes pending events at-least-once.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Enqueue

func Enqueue(ctx context.Context, db bun.IDB, topic string, payload any) error

Enqueue JSON-encodes payload and inserts a pending Event using db, which may be a *bun.DB or a bun.Tx. Pass your business transaction so the event commits atomically with your writes.

func Migrate

func Migrate(ctx context.Context, db *bun.DB) error

Migrate creates the outbox table (if it does not exist) and an index on (published_at, id) so pending-event scans stay cheap.

Types

type Event

type Event struct {
	bun.BaseModel `bun:"table:outbox,alias:o"`

	ID          int64      `bun:"id,pk,autoincrement"`
	Topic       string     `bun:"topic,notnull"`
	Payload     []byte     `bun:"payload,notnull"`
	CreatedAt   time.Time  `bun:"created_at,notnull"`
	PublishedAt *time.Time `bun:"published_at"`
}

Event is one row of the outbox. Payload holds the JSON-encoded event body. PublishedAt is nil while the event is pending and set once the relay has published it.

type Publisher

type Publisher interface {
	Publish(ctx context.Context, topic string, payload []byte) error
}

Publisher publishes a single event's raw payload bytes to its topic.

func NewRedisPublisher

func NewRedisPublisher(client redis.UniversalClient) Publisher

NewRedisPublisher returns a Publisher that publishes the raw payload bytes on the topic channel via client.Publish, without re-encoding. The channel message is therefore exactly the stored JSON, so redis.Subscribe[T] on the consumer side decodes it directly.

type Relay

type Relay struct {
	// contains filtered or unexported fields
}

Relay reads pending events from the outbox and publishes them.

func NewRelay

func NewRelay(db *bun.DB, pub Publisher, opts ...RelayOption) *Relay

NewRelay returns a Relay that publishes outbox events from db via pub.

func (*Relay) Process

func (r *Relay) Process(ctx context.Context) (int, error)

Process publishes one batch of pending events, oldest first, up to the configured batch size. Each event's PublishedAt is set only after its Publish succeeds; a Publish error stops the batch and is returned, leaving the failed event and the rest pending. It returns the number of events published. When a Locker is configured and another replica holds the lock, Process returns (0, nil).

Example

Write an event in the same transaction as the business data, then let the relay publish it. If the transaction rolls back, no event is ever published.

package main

import (
	"context"
	"database/sql"
	"fmt"

	"github.com/uptrace/bun"
	"github.com/uptrace/bun/dialect/sqlitedialect"
	"github.com/uptrace/bun/driver/sqliteshim"

	"github.com/rahmadafandi/fibr/outbox"
)

// recordingPublisher prints each event it would publish.
type recordingPublisher struct{}

func (recordingPublisher) Publish(_ context.Context, topic string, payload []byte) error {
	fmt.Printf("publish %s %s\n", topic, payload)
	return nil
}

// Write an event in the same transaction as the business data, then let the
// relay publish it. If the transaction rolls back, no event is ever published.
func main() {
	sqldb, _ := sql.Open(sqliteshim.ShimName, "file::memory:?cache=shared")
	db := bun.NewDB(sqldb, sqlitedialect.New())
	defer db.Close()
	ctx := context.Background()
	_ = outbox.Migrate(ctx, db)

	// Business write + event, atomically.
	tx, _ := db.BeginTx(ctx, nil)
	_ = outbox.Enqueue(ctx, tx, "order.created", map[string]int{"order_id": 42})
	_ = tx.Commit()

	// Background relay publishes committed events at-least-once.
	relay := outbox.NewRelay(db, recordingPublisher{})
	n, _ := relay.Process(ctx)
	fmt.Println("published:", n)
}
Output:
publish order.created {"order_id":42}
published: 1

func (*Relay) Run

func (r *Relay) Run(ctx context.Context) error

Run calls Process every interval until ctx is cancelled, returning ctx.Err(). A Process error is logged and the loop continues, so a transient publish failure does not stop the relay.

type RelayOption

type RelayOption func(*Relay)

RelayOption configures a Relay.

func WithBatchSize

func WithBatchSize(n int) RelayOption

WithBatchSize sets the maximum number of events published per Process call. The default is 100.

func WithInterval

func WithInterval(d time.Duration) RelayOption

WithInterval sets how often Run calls Process. The default is one second.

func WithLock

func WithLock(l *lock.Locker, key string, ttl time.Duration) RelayOption

WithLock makes Process run each batch under a distributed lock, so only one replica relays at a time. key and ttl are passed to lock.Do.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL