outbox

package module
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Oct 4, 2026 License: MIT Imports: 17 Imported by: 0

README

go-outbox 📮

🧭 Transactional outbox for Go on PostgreSQL: enqueue events inside your transaction and relay them at least once to RabbitMQ or any publisher.

A service that writes to its database and then publishes an event has two ways to get it wrong: the publish fails after the commit and the event is lost, or the event goes out for work that was then rolled back. go-outbox closes that gap. The event is a row written in your transaction, so it exists only if the transaction commits, and a relay ships committed rows to the broker afterwards, retrying until it succeeds.

✨ Highlights

  • 🔒 Commits with your data — Enqueue writes through the go-postgres transaction you already have; a rollback leaves nothing to publish.
  • 🔁 At least once, with a key — every message carries an idempotency key on every attempt, and Dedupe gives consumers exactly-once processing in their own transaction.
  • 🏃 Many relays, no double claims — rows are claimed with FOR UPDATE SKIP LOCKED under a lease, so relays scale out across processes.
  • ⏳ Retry, then dead-letter — capped, jittered exponential backoff; a dead-letter state after N attempts or a permanent error; Redrive to try again.
  • 🧵 Ordered per aggregate — messages that share an aggregate key go out one at a time, in enqueue order.
  • 🔔 Wakes on commit — LISTEN/NOTIFY wakes idle relays the moment a transaction commits, with polling as the fallback.
  • 🛑 Graceful shutdown — in-flight publishes finish and are recorded; unstarted claims are handed back at once.
  • 🗄️ Embedded, versioned migration — with your own table name, or the SQL for your own migration tool.
  • 🐇 RabbitMQ adapter — on go-rabbitmq with publisher confirms, in a sub-package so the core never imports AMQP.
  • 📈 Observable — logs through go-log, a Metrics hook for counters, and Stats for gauges and lag.

📦 Install

go get github.com/Bugs5382/go-outbox

It builds on go-postgres (pool and transactions), go-log (logging) and, for the adapter, go-rabbitmq. PostgreSQL 13 or later.

🚀 Usage

Create the tables once at start-up, then enqueue inside the transaction that makes the change:

ob, err := outbox.New(outbox.WithTable("orders_outbox"), outbox.WithLogger(log.NewLogger("orders")))
if err != nil {
	return err
}
if err := ob.Migrate(ctx, db); err != nil { // db is a *postgres.DB
	return err
}

err = db.RunInTx(ctx, func(tx pgx.Tx) error {
	if _, err := tx.Exec(ctx, `INSERT INTO orders (id, total) VALUES ($1, $2)`, id, total); err != nil {
		return err
	}
	_, err := ob.Enqueue(ctx, tx, outbox.Message{
		Topic:        "order.created",
		AggregateKey: "order-" + id,
		Payload:      body,
		ContentType:  "application/json",
	})
	return err
})

Enqueue returns the message's idempotency key. Leave Message.Key empty to get a random UUID, or set it from the request (a client's idempotency token, say) so a replay is caught: a key that is already in the table returns ErrDuplicateKey, and the transaction stays usable. RunInTx may retry the whole function on a serialization failure, which is safe: the retried transaction enqueues again and the rolled-back attempt left nothing behind.

Run a relay beside the service, or in a worker of its own:

conn, err := rabbitmq.Connect(ctx, amqpURL)
if err != nil {
	return err
}
pub := outboxrabbitmq.New(conn, "events") // github.com/Bugs5382/go-outbox/rabbitmq

relay := ob.NewRelay(db, pub)
go func() { _ = relay.Run(ctx) }() // returns nil once ctx ends and the relay has drained

🧾 Consuming: deduplicate on the key

Delivery is at least once, so a consumer sees a message again when a relay stopped between publishing it and recording it. The RabbitMQ adapter sends the idempotency key as the AMQP message id. Dedupe records the key in the consumer's own transaction and reports whether it is new; if the handler fails and rolls back, the record goes with it and the redelivery is processed:

handler := func(ctx context.Context, d rabbitmq.Delivery) error {
	return db.RunInTx(ctx, func(tx pgx.Tx) error {
		fresh, err := ob.Dedupe(ctx, tx, "invoicing", d.MessageID)
		if err != nil || !fresh {
			return err // a duplicate is acknowledged and skipped
		}
		return createInvoice(ctx, tx, d.Body)
	})
}

Each consumer name keeps its own record. A consumer in another service can use its own Outbox (and Migrate) just for Dedupe.

⚙️ How the relay works

Each cycle the relay claims up to a batch of due rows in one statement: pending, past their backoff, not under a live lease and, for rows with an aggregate key, the oldest pending row of that aggregate. FOR UPDATE SKIP LOCKED lets concurrent relays pass over each other's rows, and the claim stamps a lease and bumps the attempt count. The relay then publishes outside any transaction, so no connection or lock is held across the network call, and records each outcome:

Publish result Row becomes
success sent
error, attempts left pending, due again after the backoff
error on the last attempt, or outbox.Permanent(err) dead, with the last error kept
the relay stopped before marking it still pending; claimable again when the lease runs out

The attempt count fences every update: a relay whose lease ran out and whose row was reclaimed cannot overwrite the new owner's state. The lease (WithLease, default 30s) must comfortably exceed the time to publish one batch, since a lease that expires mid-publish lets another relay send the same rows again.

Wake-up. Enqueue queues a NOTIFY on a channel named after the table, which PostgreSQL delivers only on commit. Each relay holds a LISTEN on a connection it takes out of the pool, and reconnects with backoff when it drops. Polling (WithPollInterval, default 2s) still runs: it picks up backoff expiries, delayed messages (Message.Delay), expired leases and anything committed while the listener was down. Behind a pooler in transaction mode, which cannot hold a LISTEN, use WithoutNotify().

Ordering. Only the oldest pending row of an aggregate can be claimed, so its successors wait while it backs off. A row that becomes a dead letter stops blocking its aggregate; the ones after it are then published, and Redrive sends the dead one later, out of order. Rows without an aggregate key are claimed in id order but published concurrently (WithConcurrency, default 4).

Shutdown. When Run's context ends the relay claims nothing more. Publishes already under way get the drain timeout (WithDrainTimeout, default 10s) to finish, and their outcome is recorded. Rows it had claimed but not started are released at once, with their attempt count restored, so another relay picks them up without waiting for the lease.

🎛 Options

Option Default What it sets
WithTable(name) outbox Table name, optionally schema.name; also names the inbox, migration table and notify channel
WithLogger(l) no-op go-log Logger for the outbox and its relays
WithMetrics(m) no-op Metrics hook for relay events
WithBatchSize(n) 100 Rows claimed per cycle
WithConcurrency(n) 4 Rows published at the same time
WithLease(d) 30s How long a claim holds its rows
WithPollInterval(d) 2s Idle poll interval
WithMaxAttempts(n) 10 Attempts before a dead letter
WithBackoff(min, max) 1s, 5m Retry delay range
WithDrainTimeout(d) 10s Time in-flight publishes get on shutdown
WithoutNotify() notify on Polling only

The first three are outbox.Options for New; the rest are RelayOptions for NewRelay.

🗄️ Schema and operations

Migrate is safe on every start and from several processes at once: it runs under an advisory lock in one transaction and records applied versions in <table>_migrations. It creates:

  • <table>: the outbox, with a unique idempotency key, pending/sent/dead status, attempts, backoff and lease times, the last error, and partial indexes for the claim query;
  • <table>_inbox: the consumer and key pairs Dedupe records.

To run the schema through your own migration tool instead, take the statements from ob.SQL().

Call Use
Stats(ctx, q) Pending, in-flight, sent and dead counts, and the age of the oldest pending row
Redrive(ctx, q, keys...) Return dead letters (all, or the listed keys) to pending with fresh attempts
Purge(ctx, q, age) Delete sent rows older than age; run it on a schedule

🐇 RabbitMQ publisher

outboxrabbitmq.New(conn, exchange, opts...) publishes each record to exchange with Topic as the routing key, the idempotency key as the message id, ContentType, the record's headers, the enqueue time as the timestamp, and two headers of its own: x-outbox-attempt and, when set, x-outbox-aggregate-key. Publisher confirms are always on and messages are persistent, so a row is marked sent only after the broker has it. Pass rabbitmq.WithMandatory() to have an unroutable message come back as an error, and so be retried, instead of being dropped.

📈 Logs and metrics

Lifecycle goes to info, cycles and publishes to debug, per-message detail to trace, retries and listener drops to warn, and dead letters and bookkeeping failures to error. Payloads are never logged; messages are identified by key and topic. Metrics receives Claimed, Published (with the duration), Failed, DeadLettered and Released; embed outbox.NopMetrics in an implementation to stay compatible as the interface grows.

🛠 Develop

task build              # go build ./...
task test               # go test ./... (unit tests, no services)
task test-integration   # integration tests; testcontainers starts PostgreSQL and RabbitMQ in Docker
task lint               # gofmt check + golangci-lint + yamllint
task license            # check MIT headers (golic)

⚖️ License

MIT (c) 2026 Shane

Documentation

Overview

Package outbox is a transactional outbox for PostgreSQL: a service publishes an event if and only if the database transaction that caused it commits.

Writing

Enqueue writes a Message to the outbox table through the caller's own transaction, the one from go-postgres's RunInTx or RunInTxQuerier. The row commits or rolls back with the business change, so a rolled-back transaction never publishes anything and a committed one always does. Enqueue also queues a NOTIFY, which PostgreSQL delivers only on commit.

Relaying

A Relay, built with NewRelay, claims due rows in batches with FOR UPDATE SKIP LOCKED and stamps each with a lease, then publishes them through a Publisher outside any transaction and marks each one sent. Any number of relays, in any number of processes, can drain one table: SKIP LOCKED and the lease keep two relays from claiming the same row. A relay sleeps between cycles until a notification or the poll interval wakes it.

Guarantees

  • At least once. A relay that dies between publishing and marking leaves its rows leased; when the lease runs out another relay publishes them again. Every attempt carries the same idempotency key (Message.Key), and consumers deduplicate on it, with Dedupe or their own store.
  • Retries. A failed publish is retried after an exponential, jittered backoff. After the configured number of attempts, or at once for an error wrapped with Permanent, the row becomes a dead letter. Redrive puts dead letters back.
  • Ordering. Messages that share an AggregateKey are published one at a time in enqueue order: a row is claimable only while no older row of its aggregate is still pending. A dead letter stops blocking its aggregate. Messages without an AggregateKey have no ordering.
  • Graceful shutdown. When Run's context ends the relay stops claiming, lets in-flight publishes finish within the drain timeout and records them, and releases the rows it had claimed but not started.

Schema

Migrate creates the outbox table, an inbox table for Dedupe, and a table that records applied schema versions, all named after WithTable (default "outbox"). SQL returns the same statements for an external migration tool. PostgreSQL 13 or later is required.

Publishers and observability

Publisher is a single method, so any broker fits. The rabbitmq sub-package is a ready one for RabbitMQ on go-rabbitmq, with publisher confirms. The outbox logs through a go-log Logger (WithLogger) and reports relay events to a Metrics hook (WithMetrics); Stats gives counts and lag for gauges and health checks.

Example
package main

import (
	"context"
	"time"

	log "github.com/Bugs5382/go-log"
	outbox "github.com/Bugs5382/go-outbox"

	outboxrabbitmq "github.com/Bugs5382/go-outbox/rabbitmq"

	postgres "github.com/Bugs5382/go-postgres"

	rabbitmq "github.com/Bugs5382/go-rabbitmq"
	"github.com/jackc/pgx/v5"
)

func main() {
	ctx := context.Background()
	db, err := postgres.New(ctx, "postgres://app:secret@localhost:5432/app")
	if err != nil {
		return
	}
	defer db.Close()

	ob, err := outbox.New(outbox.WithTable("app_outbox"), outbox.WithLogger(log.NewLogger("orders")))
	if err != nil {
		return
	}
	if err := ob.Migrate(ctx, db); err != nil {
		return
	}

	// The order and its event commit together, or not at all.
	_ = db.RunInTx(ctx, func(tx pgx.Tx) error {
		if _, err := tx.Exec(ctx, `INSERT INTO orders (id) VALUES ($1)`, 42); err != nil {
			return err
		}
		_, err := ob.Enqueue(ctx, tx, outbox.Message{
			Topic:        "order.created",
			AggregateKey: "order-42",
			Payload:      []byte(`{"id":42}`),
			ContentType:  "application/json",
		})
		return err
	})

	conn, err := rabbitmq.Connect(ctx, "amqp://app:secret@localhost:5672/")
	if err != nil {
		return
	}
	defer func() { _ = conn.Close() }()
	pub := outboxrabbitmq.New(conn, "events")
	defer func() { _ = pub.Close() }()

	relay := ob.NewRelay(db, pub, outbox.WithMaxAttempts(8))
	runCtx, stop := context.WithTimeout(ctx, time.Minute)
	defer stop()
	_ = relay.Run(runCtx)
}

Index

Examples

Constants

View Source
const DefaultTable = "outbox"

DefaultTable is the outbox table name used when WithTable is not given.

Variables

View Source
var (
	// ErrInvalidTable is returned by New for a table name that is not a
	// plain, optionally schema-qualified, lowercase identifier.
	ErrInvalidTable = errors.New("outbox: invalid table name")
	// ErrInvalidMessage is returned by Enqueue and Dedupe for input that is
	// missing a required field.
	ErrInvalidMessage = errors.New("outbox: invalid message")
	// ErrDuplicateKey is returned by Enqueue when the idempotency key is
	// already in the outbox. The caller's transaction is still usable.
	ErrDuplicateKey = errors.New("outbox: duplicate idempotency key")
)

Functions

func IsPermanent

func IsPermanent(err error) bool

IsPermanent reports whether err, or any error it wraps, was marked with Permanent.

func Permanent

func Permanent(err error) error

Permanent marks err as not worth retrying. A Publisher returns it for a message the broker will never accept (a malformed payload, a rejected destination), and the relay dead-letters the row at once instead of spending its remaining attempts. Permanent(nil) is nil.

Example
package main

import (
	"context"
	"errors"

	outbox "github.com/Bugs5382/go-outbox"
)

func main() {
	pub := outbox.PublisherFunc(func(ctx context.Context, r outbox.Record) error {
		if len(r.Payload) == 0 {
			return outbox.Permanent(errors.New("empty payload"))
		}
		return nil
	})
	_ = pub
}

Types

type Message

type Message struct {
	// Key is the idempotency key. It is unique per outbox table, travels with
	// every delivery attempt, and is what consumers deduplicate on. Leave it
	// empty to have a random UUID generated.
	Key string
	// Topic says where the message goes; the RabbitMQ publisher uses it as
	// the routing key. Required.
	Topic string
	// AggregateKey scopes ordering: messages that share one are published
	// one at a time, in enqueue order. Empty means no ordering constraint.
	AggregateKey string
	// Payload is the message body, stored as bytes.
	Payload []byte
	// ContentType describes Payload, such as application/json.
	ContentType string
	// Headers are carried to the publisher unchanged.
	Headers map[string]string
	// Delay holds the message back for this long after the enqueue.
	Delay time.Duration
}

Message is one event to publish once the transaction that enqueues it commits.

type Metrics

type Metrics interface {
	Claimed(n int)
	Published(topic string, attempt int, took time.Duration)
	Failed(topic string, attempt int, err error)
	DeadLettered(topic string, attempts int)
	Released(n int)
}

Metrics receives relay events.

type NopMetrics

type NopMetrics struct{}

NopMetrics discards every event.

func (NopMetrics) Claimed

func (NopMetrics) Claimed(int)

func (NopMetrics) DeadLettered

func (NopMetrics) DeadLettered(string, int)

func (NopMetrics) Failed

func (NopMetrics) Failed(string, int, error)

func (NopMetrics) Published

func (NopMetrics) Published(string, int, time.Duration)

func (NopMetrics) Released

func (NopMetrics) Released(int)

type Option

type Option func(*config)

Option configures an Outbox.

func WithLogger

func WithLogger(l log.Logger) Option

WithLogger sets the go-log logger the outbox and its relays write to. The default discards everything.

func WithMetrics

func WithMetrics(m Metrics) Option

WithMetrics sets the hook that receives relay events. The default discards them.

func WithTable

func WithTable(name string) Option

WithTable sets the outbox table name, optionally schema-qualified ("audit.events"). Each part is lowercase letters, digits and underscores, starts with a letter or underscore, and is at most 50 characters. The inbox and migration tables are named after it, with _inbox and _migrations suffixes, and the notification channel is the table name.

type Outbox

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

Outbox writes messages to an outbox table, inside the caller's transaction, and builds relays that drain that table. An Outbox holds no connection and is safe for concurrent use.

func New

func New(opts ...Option) (*Outbox, error)

New builds an Outbox. It returns ErrInvalidTable when the table name is not a plain, optionally schema-qualified, lowercase identifier.

func (*Outbox) Dedupe

func (o *Outbox) Dedupe(ctx context.Context, tx postgres.Querier, consumer, key string) (bool, error)

Dedupe is the consumer half of exactly-once processing on top of at-least-once delivery. It records that consumer has handled the message with key and reports whether this is the first time. Call it in the same transaction as the consumer's own writes: when it returns false the message was already handled and can be acknowledged without doing anything; when the transaction rolls back, the record goes with it and a redelivery is fresh again. Each consumer name keeps its own record of keys.

Example
package main

import (
	"context"

	outbox "github.com/Bugs5382/go-outbox"

	postgres "github.com/Bugs5382/go-postgres"

	rabbitmq "github.com/Bugs5382/go-rabbitmq"
	"github.com/jackc/pgx/v5"
)

func main() {
	ctx := context.Background()
	db, err := postgres.New(ctx, "postgres://app:secret@localhost:5432/app")
	if err != nil {
		return
	}
	defer db.Close()
	ob, _ := outbox.New()

	handle := func(ctx context.Context, d rabbitmq.Delivery) error {
		return db.RunInTx(ctx, func(tx pgx.Tx) error {
			fresh, err := ob.Dedupe(ctx, tx, "invoicing", d.MessageID)
			if err != nil || !fresh {
				return err
			}
			_, err = tx.Exec(ctx, `INSERT INTO invoices (order_id) VALUES ($1)`, string(d.Body))
			return err
		})
	}
	_ = handle
}

func (*Outbox) Enqueue

func (o *Outbox) Enqueue(ctx context.Context, tx postgres.Querier, msg Message) (string, error)

Enqueue writes msg to the outbox through tx and returns its idempotency key. Pass the transaction that makes the change the message announces (from postgres.DB.RunInTx or RunInTxQuerier): the row, and the wake-up notification for idle relays, then exist only if that transaction commits.

A key that is already in the table returns ErrDuplicateKey without aborting tx, so the caller can treat a replayed request as done. A message without a Topic returns ErrInvalidMessage.

func (*Outbox) Migrate

func (o *Outbox) Migrate(ctx context.Context, db *postgres.DB) error

Migrate creates the outbox and inbox tables, or brings them up to date. It is safe to run on every start and from several processes at once: the steps run in one transaction under an advisory lock, and each applied version is recorded in the <table>_migrations table, so a step never runs twice. A schema-qualified table needs its schema to exist already.

To run the schema through a migration tool of your own instead, take the statements from SQL.

func (*Outbox) NewRelay

func (o *Outbox) NewRelay(db *postgres.DB, pub Publisher, opts ...RelayOption) *Relay

NewRelay builds a relay that drains this outbox through db and sends through pub. It does nothing until Run or RunOnce.

func (*Outbox) Purge

func (o *Outbox) Purge(ctx context.Context, q postgres.Querier, age time.Duration) (int64, error)

Purge deletes sent rows older than age and reports how many it removed. Pending and dead rows are never purged. Run it on a schedule; the relay does not delete anything by itself.

func (*Outbox) Redrive

func (o *Outbox) Redrive(ctx context.Context, q postgres.Querier, keys ...string) (int64, error)

Redrive returns dead letters to pending with a fresh attempt count, so a relay publishes them again. With no keys it redrives every dead letter. It reports how many rows it changed.

func (*Outbox) SQL

func (o *Outbox) SQL() ([]string, error)

SQL returns the schema statements Migrate runs, rendered for this outbox's table names, one string per migration in version order. Use it to feed the schema to your own migration tool instead of calling Migrate.

func (*Outbox) Stats

func (o *Outbox) Stats(ctx context.Context, q postgres.Querier) (Stats, error)

Stats reports the outbox counts, for a gauge or a health check.

func (*Outbox) Table

func (o *Outbox) Table() string

Table returns the outbox table name as configured.

type Publisher

type Publisher interface {
	Publish(ctx context.Context, r Record) error
}

Publisher sends a record to the broker.

type PublisherFunc

type PublisherFunc func(ctx context.Context, r Record) error

PublisherFunc adapts a function to Publisher.

func (PublisherFunc) Publish

func (f PublisherFunc) Publish(ctx context.Context, r Record) error

Publish calls f.

type Record

type Record struct {
	Message
	ID        int64
	Attempt   int
	CreatedAt time.Time
}

Record is a claimed outbox row handed to a Publisher.

type Relay

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

Relay moves committed messages from the outbox to a Publisher. Each cycle it claims a batch of due rows under a lease (FOR UPDATE SKIP LOCKED, so any number of relays can run against one table without claiming the same row), publishes them, and marks each one sent, scheduled for a retry, or dead.

Delivery is at least once: a relay that stops between publishing a row and marking it leaves the row leased, and once the lease runs out another relay publishes it again with the same idempotency key.

func (*Relay) Listening

func (r *Relay) Listening() bool

Listening reports whether the relay currently holds its LISTEN subscription. It is false with WithoutNotify, before Run starts, and while the notification connection is being re-established.

func (*Relay) Run

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

Run drains the outbox until ctx ends, then shuts down gracefully: it stops claiming, lets publishes already under way finish (up to the drain timeout) and records their outcome, and releases claimed rows it had not started so another relay can take them at once. It returns nil after a shutdown. Errors along the way are logged and retried, never returned.

func (*Relay) RunOnce

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

RunOnce runs a single cycle: it claims up to one batch of due rows, publishes them and records each outcome. It returns how many rows it claimed. Run calls it in a loop; call it yourself to drive the relay from a scheduler or a test. When ctx ends part way, rows not yet started are released and rows in flight finish within the drain timeout.

type RelayOption

type RelayOption func(*relayConfig)

RelayOption configures a Relay.

func WithBackoff

func WithBackoff(minDelay, maxDelay time.Duration) RelayOption

WithBackoff sets the retry delay range (default 1s to 5m). The delay doubles per attempt from minDelay, is capped at maxDelay, and is jittered into the upper half of that value.

func WithBatchSize

func WithBatchSize(n int) RelayOption

WithBatchSize sets how many rows one cycle claims (default 100).

func WithConcurrency

func WithConcurrency(n int) RelayOption

WithConcurrency sets how many claimed rows are published at the same time (default 4). Rows of one aggregate are never in flight together, whatever the setting.

func WithDrainTimeout

func WithDrainTimeout(d time.Duration) RelayOption

WithDrainTimeout bounds how long Run waits, after its context ends, for in-flight publishes to finish (default 10s).

func WithLease

func WithLease(d time.Duration) RelayOption

WithLease sets how long a claim holds its rows (default 30s). It must comfortably exceed the time to publish one batch: a lease that runs out mid-publish lets another relay send the same rows again. After a crash, the lease is also how long the crashed relay's rows wait to be reclaimed.

func WithMaxAttempts

func WithMaxAttempts(n int) RelayOption

WithMaxAttempts sets how many publish attempts a row gets before it is dead-lettered (default 10).

func WithPollInterval

func WithPollInterval(d time.Duration) RelayOption

WithPollInterval sets how often an idle relay looks for due rows (default 2s). Notifications wake it sooner; polling is what finds retries whose backoff has passed, delayed messages, expired leases, and rows committed while the notification connection was down.

func WithoutNotify

func WithoutNotify() RelayOption

WithoutNotify turns off the LISTEN/NOTIFY wake-up, leaving polling only. Use it behind a connection pooler in transaction mode, which cannot hold a LISTEN.

type Stats

type Stats struct {
	// Pending rows wait to be published, including rows backing off after
	// a failed attempt. Rows a relay holds right now are in InFlight instead.
	Pending int64
	// InFlight rows are claimed by a relay under an unexpired lease.
	InFlight int64
	// Sent rows were published and are kept until Purge removes them.
	Sent int64
	// Dead rows ran out of attempts or failed permanently.
	Dead int64
	// OldestPending is the age of the oldest pending or in-flight row, the
	// relay's lag. Zero when nothing is waiting.
	OldestPending time.Duration
}

Stats counts the outbox rows by state.

Directories

Path Synopsis
Package rabbitmq is the outbox Publisher for RabbitMQ, built on github.com/Bugs5382/go-rabbitmq.
Package rabbitmq is the outbox Publisher for RabbitMQ, built on github.com/Bugs5382/go-rabbitmq.

Jump to

Keyboard shortcuts

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