outbox

package
v0.10.3 Latest Latest
Warning

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

Go to latest
Published: Jul 17, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

Documentation

Overview

Package outbox provides transaction-time recording and relay primitives for reliable integration message publishing.

Record messages through Recorder inside the same transaction as the business write. Publish them through Relay after commit. Delivery is at-least-once, so consumers must deduplicate by the message ID.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	ErrNilStore            = errors.New("outbox: nil store")
	ErrNilCodec            = errors.New("outbox: nil codec")
	ErrNilPublisher        = errors.New("outbox: nil publisher")
	ErrInvalidClaimOptions = errors.New("outbox: invalid claim options")
	ErrInvalidRunOptions   = errors.New("outbox: invalid run options")
	ErrUnknownKind         = errors.New("outbox: unknown message kind")
	ErrNilFactory          = errors.New("outbox: nil protobuf factory")
)

Functions

This section is empty.

Types

type ClaimOptions

type ClaimOptions struct {
	Limit       int
	Now         time.Time
	LockedUntil time.Time
	ClaimedBy   string
}

func NormalizeClaimOptions

func NormalizeClaimOptions(opts ClaimOptions, now func() time.Time) (ClaimOptions, error)

NormalizeClaimOptions fills default claim timestamps and validates the claim window before a store attempts to lock records.

type Codec

type Codec interface {
	Encode(message.Message) (Record, error)
	Decode(Record) (message.Message, error)
}

type FixedBackoffPolicy

type FixedBackoffPolicy struct {
	MaxAttempts int
	Backoff     time.Duration
}

func (FixedBackoffPolicy) NextAttempt

func (p FixedBackoffPolicy) NextAttempt(record Record, err error, now time.Time) RetryDecision

type NoRetryPolicy

type NoRetryPolicy struct{}

func (NoRetryPolicy) NextAttempt

func (NoRetryPolicy) NextAttempt(_ Record, err error, _ time.Time) RetryDecision

type ProtoCodec

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

func NewProtoCodec

func NewProtoCodec(opts ...ProtoCodecOption) *ProtoCodec

NewProtoCodec creates a protobuf-backed outbox codec.

func (*ProtoCodec) Decode

func (c *ProtoCodec) Decode(record Record) (message.Message, error)

func (*ProtoCodec) Encode

func (c *ProtoCodec) Encode(msg message.Message) (Record, error)

func (*ProtoCodec) Register

func (c *ProtoCodec) Register(kind message.Kind, factory func() proto.Message) error

Register adds a protobuf factory to the codec's default payload registry.

type ProtoCodecOption added in v0.9.1

type ProtoCodecOption func(*ProtoCodec)

ProtoCodecOption configures a protobuf outbox codec.

func WithPayloadResolver added in v0.9.1

func WithPayloadResolver(resolver message.PayloadResolver) ProtoCodecOption

WithPayloadResolver makes the codec use resolver when decoding records.

The default resolver is the codec's internal registry populated through Register. Supplying a shared resolver lets outbox and broker adapters reuse the same Kind-to-protobuf mapping.

type Record

type Record struct {
	ID         string
	MessageID  string
	Kind       message.Kind
	Key        string
	OccurredAt time.Time
	Payload    []byte
	Headers    map[string]string

	Status        Status
	Attempts      int
	NextAttemptAt time.Time
	LockedUntil   time.Time
	ClaimedBy     string
	LastError     string
	CreatedAt     time.Time
	UpdatedAt     time.Time
}

func (Record) Clone

func (r Record) Clone() Record

type Recorder

type Recorder interface {
	Record(ctx context.Context, messages ...message.Message) error
}

type RecorderOption

type RecorderOption func(*recorderConfig)

type Relay

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

func NewRelay

func NewRelay(store Store, codec Codec, publisher message.Publisher, opts ...RelayOption) (*Relay, error)

func (*Relay) Run

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

func (*Relay) RunOnce

func (r *Relay) RunOnce(ctx context.Context, opts ClaimOptions) RunResult
Example
package main

import (
	"context"
	"fmt"
	"time"

	"github.com/go-jimu/components/ddd/message"
	"github.com/go-jimu/components/ddd/message/outbox"
	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/types/known/wrapperspb"
)

type exampleOutboxStore struct {
	records   []outbox.Record
	published []string
}

func (s *exampleOutboxStore) Append(_ context.Context, records ...outbox.Record) error {
	for _, record := range records {
		s.records = append(s.records, record.Clone())
	}
	return nil
}

func (s *exampleOutboxStore) Claim(_ context.Context, opts outbox.ClaimOptions) ([]outbox.Record, error) {
	limit := opts.Limit
	if limit > len(s.records) {
		limit = len(s.records)
	}
	claimed := make([]outbox.Record, 0, limit)
	for i := 0; i < limit; i++ {
		record := s.records[i].Clone()
		record.Status = outbox.StatusProcessing
		record.LockedUntil = opts.LockedUntil
		record.ClaimedBy = opts.ClaimedBy
		claimed = append(claimed, record)
	}
	return claimed, nil
}

func (s *exampleOutboxStore) MarkPublished(_ context.Context, records ...outbox.Record) error {
	for _, record := range records {
		s.published = append(s.published, record.MessageID)
	}
	return nil
}

func (s *exampleOutboxStore) MarkFailed(_ context.Context, record outbox.Record, reason string, nextAttemptAt time.Time) error {
	return nil
}

type exampleOutboxPublisher struct {
	kinds []message.Kind
}

func (p *exampleOutboxPublisher) Publish(_ context.Context, msg message.Message) error {
	p.kinds = append(p.kinds, msg.Kind())
	return nil
}

func main() {
	codec := outbox.NewProtoCodec()
	if err := codec.Register("customer.created", func() proto.Message {
		return &wrapperspb.StringValue{}
	}); err != nil {
		panic(err)
	}

	msg, err := message.New(
		"customer.created",
		wrapperspb.String("alice"),
		message.WithID("msg-1"),
		message.WithKey("customer-1"),
		message.WithOccurredAt(time.Unix(0, 0).UTC()),
	)
	if err != nil {
		panic(err)
	}

	store := &exampleOutboxStore{}
	recorder, err := outbox.NewRecorder(store, codec)
	if err != nil {
		panic(err)
	}
	if err := recorder.Record(context.Background(), msg); err != nil {
		panic(err)
	}

	publisher := &exampleOutboxPublisher{}
	relay, err := outbox.NewRelay(store, codec, publisher, outbox.WithClock(func() time.Time {
		return time.Unix(10, 0).UTC()
	}))
	if err != nil {
		panic(err)
	}

	result := relay.RunOnce(context.Background(), outbox.ClaimOptions{
		Limit:       10,
		LockedUntil: time.Unix(20, 0).UTC(),
		ClaimedBy:   "relay-1",
	})

	fmt.Println(len(store.records), result.Claimed, result.Published, publisher.kinds[0], store.published[0])

}
Output:
1 1 1 customer.created msg-1

type RelayOption

type RelayOption func(*relayConfig)

func WithClock

func WithClock(now func() time.Time) RelayOption

func WithRetryPolicy

func WithRetryPolicy(policy RetryPolicy) RelayOption

type RetryDecision

type RetryDecision struct {
	Retry         bool
	NextAttemptAt time.Time
	Reason        string
}

type RetryPolicy

type RetryPolicy interface {
	NextAttempt(record Record, err error, now time.Time) RetryDecision
}

type RunOptions

type RunOptions struct {
	Claim    ClaimOptions
	Interval time.Duration
	OnResult func(RunResult)
}

type RunResult

type RunResult struct {
	Claimed   int
	Published int
	// Failed counts decode or publish failures that were successfully persisted
	// through Store.MarkFailed.
	Failed int
	Errors []error
}

type Status

type Status string
const (
	StatusPending    Status = "pending"
	StatusProcessing Status = "processing"
	StatusPublished  Status = "published"
	StatusFailed     Status = "failed"
)

type Store

type Store interface {
	Append(ctx context.Context, records ...Record) error
	Claim(ctx context.Context, opts ClaimOptions) ([]Record, error)
	MarkPublished(ctx context.Context, records ...Record) error
	MarkFailed(ctx context.Context, record Record, reason string, nextAttemptAt time.Time) error
}

type StoreRecorder

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

func NewRecorder

func NewRecorder(store Store, codec Codec, opts ...RecorderOption) (*StoreRecorder, error)

func (*StoreRecorder) Record

func (r *StoreRecorder) Record(ctx context.Context, messages ...message.Message) error

Jump to

Keyboard shortcuts

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