outbox

module
v0.10.0 Latest Latest
Warning

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

Go to latest
Published: Jul 14, 2026 License: MIT

README

GO Outbox (Core)

github.com/assurrussa/outbox is the core outbox library.

The repository is now a multi-module monorepo:

  • core module: github.com/assurrussa/outbox
  • MySQL backend: github.com/assurrussa/outbox/backends/mysql
  • SQLite backend: github.com/assurrussa/outbox/backends/sqlite
  • Postgres backend: github.com/assurrussa/outbox/backends/pgsql
  • Picodata backend: github.com/assurrussa/outbox/backends/picodata

Install core

go get github.com/assurrussa/outbox@latest
import "github.com/assurrussa/outbox/outbox"

Core usage

package main

import (
	"context"
	"time"

	"github.com/assurrussa/outbox/outbox"
	outboxlogger "github.com/assurrussa/outbox/outbox/logger"
	sharedjob "github.com/assurrussa/outbox/shared/job"
)

type SendEmailJob struct {
	sharedjob.DefaultJob
}

func (*SendEmailJob) Name() string { return "send_email" }

func (*SendEmailJob) Handle(_ context.Context, _ string) error { return nil }

func main() {
	ctx := context.Background()

	svc, err := outbox.New(
		outbox.WithWorkers(1),
		outbox.WithReserveFor(time.Second),
		outbox.WithIdleTime(5*time.Minute),
		outbox.WithLogger(outboxlogger.Default()),
		outbox.WithJobsRepo(jobsRepo),
		// Optional: only needed for svc.GetQueueStats(...)
		outbox.WithJobsStatRepo(jobsRepo),
		outbox.WithJobsFailedRepo(jobsFailedRepo),
		outbox.WithTransactor(txManager),
	)
	if err != nil {
		panic(err)
	}

	emailJob := &SendEmailJob{}
	svc.MustRegisterJob(emailJob)

	go func() { _ = svc.Run(ctx) }()
	_, _ = svc.Put(ctx, "send_email", `{"id":"1"}`, time.Now())
}

For a graceful worker shutdown, first remove the process from readiness, then call svc.BeginDrain() without cancelling the Run context. After BeginDrain returns, no new repository claim can start; already reserved capability jobs keep their fenced lease heartbeat and may ack normally. Cancel the Run context only when the host's bounded drain deadline expires, so an unfinished handler cannot ack and its lease can be recovered by another worker. svc.Readiness(ctx) is a non-mutating worker-lifecycle probe: it is successful only after worker loops start and becomes unavailable before BeginDrain closes claim admission. Pair it with a separate database probe; readiness never reserves a synthetic outbox job.

JobsStatRepository is optional.
Set WithJobsStatRepo(...) only if you need svc.GetQueueStats(...).

Capability-aware workers (opt-in)

Use capability mode when workers must claim only payload schemas they can decode. It is additive: legacy Put, repositories, and unknown-job/DLQ behavior stay unchanged until both capability repositories are configured.

type PublishV2Job struct {
	sharedjob.DefaultJob
}

func (*PublishV2Job) Name() string { return "cms.entry.publish" }

func (*PublishV2Job) SchemaVersion() outbox.SchemaVersion { return 2 }

func (*PublishV2Job) Handle(_ context.Context, _ string) error { return nil }

svc, err := outbox.New(
	outbox.WithJobsRepo(jobsRepo),
	outbox.WithCapabilityJobsRepo(jobsRepo),
	outbox.WithJobsFailedRepo(jobsFailedRepo),
	outbox.WithCapabilityJobsFailedRepo(jobsFailedRepo),
	outbox.WithTransactor(txManager),
)
if err != nil {
	panic(err)
}

svc.MustRegisterJob(&PublishV2Job{})
_, _ = svc.PutVersioned(
	ctx,
	"cms.entry.publish",
	2,
	`{"entryId":"1"}`,
	time.Now(),
)

Jobs without SchemaVersion() use schema v1. In capability mode, ordinary Put(...) also persists schema v1. Claims are filtered by the registered (name, schemaVersion) set, so unsupported jobs remain pending and do not move to DLQ. Active handlers refresh their lease every reserveFor / 3; successful ack and DLQ deletion require the same live lease token.

Rollout rule: do not enqueue schemas newer than v1 while legacy workers are still running. Deploy capability-aware workers that understand both versions, remove legacy workers, and only then enable the new producer schema.

PostgreSQL, MySQL, and SQLite implement the complete capability and durable fan-out contracts. Picodata implements versioned/CAS capability storage only; it deliberately omits fan-out and standard runtime composition until its client can provide a real atomic transaction boundary.

Durable fan-out (opt-in)

Configure WithFanoutJobsRepo(jobsRepo) together with capability mode when one integration event must produce independently retried deliveries. PutFanout stores the event plus the exact eligible target snapshots under the event ID. The built-in dispatcher is registered during construction and creates one job per target in a transaction.

event := outbox.FanoutEvent{
	ID:            types.NewMessageID(),
	Topic:         "cms.entry.published",
	SchemaVersion: 1,
	Payload:       json.RawMessage(`{"entryId":"1"}`),
	OccurredAt:    time.Now(),
}

_, err = svc.PutFanout(ctx, event, []outbox.FanoutTarget{
	{
		Kind:     "nitro",
		ID:       "site-1",
		Snapshot: json.RawMessage(`{"namespace":"public"}`),
	},
	{
		Kind:     "webhook",
		ID:       "subscription-7",
		Snapshot: json.RawMessage(`{"configRevision":4,"secretRevision":2}`),
	},
}, time.Now())

Delivery handlers register FanoutDeliveryJobName(targetKind, eventTopic) for the event schema they understand and decode payloads with DecodeFanoutDelivery. Every delivery has a deterministic ID suitable for webhook idempotency. Unsupported delivery capabilities stay pending with zero attempts.

Fan-out retries are idempotent even after a delivery job was acknowledged and deleted: PostgreSQL, MySQL, and SQLite retain a compact key/fingerprint tombstone separately from active jobs. Prune tombstones in bounded batches only after the host's event replay, audit, and webhook retry retention windows have elapsed.

Backend modules

Pick only the backend module you need for a project.

Example app

Runnable examples:

Migration from old import paths

Old infrastructure/* paths were removed (hard break).

See MIGRATION.md.

Notes on shared/*

shared/* is kept in core for internal library/backend reuse, but should be treated as unstable internal API by external consumers.

Development

Use workspace-aware commands:

make test-core
make test-backends
make test-integration-all

For integration services:

make devup
make devdown

Release prep for backend modules:

# pin backend modules to a published core tag
make release-ready-backends CORE_VERSION=v0.10.0

# verify each backend as standalone module (without go.work)
make release-verify-backends

# non-mutating exact-version pre-tag gate
make release-readiness-backends CORE_VERSION=v0.10.0

License

MIT. See LICENSE.

Directories

Path Synopsis
backends
pgsql module
picodata module
sqlite module
jobs/payload
Package payload contains helper-packages common payload.
Package payload contains helper-packages common payload.
mocks
Package outboxmocks is a generated GoMock package.
Package outboxmocks is a generated GoMock package.
shared
job
tools
toolsmocks command

Jump to

Keyboard shortcuts

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