inbox

package
v0.61.0 Latest Latest
Warning

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

Go to latest
Published: Aug 31, 2026 License: MIT Imports: 26 Imported by: 0

Documentation

Overview

Package inbox provides durable consumer-side idempotency: a ledger that records processed event ids so redeliveries are skipped. It is the consumer-side complement to the transactional outbox.

Consumers extract the event id from the delivery (e.g. via outbox.EventIDFromHeaders) and wrap their handler in deps.Inbox.ProcessOnce, which records the id and runs the handler atomically, exactly once per id.

Index

Constants

View Source
const (
	DefaultHoldTableName     = "gobricks_inbox_hold"
	DefaultHoldDrainInterval = 5 * time.Second
	DefaultHoldMaxBackoff    = 5 * time.Minute
	DefaultHoldMaxAge        = time.Hour
	DefaultHoldLeaseDuration = 60 * time.Second
)

The hold's defaults. They are applied only when the hold is enabled, so a deployment reading inbox.hold.* back does not find settings for a hold it never asked for.

View Source
const DefaultRetentionPeriod = 7 * 24 * time.Hour

DefaultRetentionPeriod is the default processed-event retention (7 days). It must exceed the broker's maximum redelivery window. Written as a duration (168h) because Go's time.ParseDuration does not accept a "7d" unit.

View Source
const DefaultTableName = "gobricks_inbox"

DefaultTableName is the default inbox ledger table name.

Variables

This section is empty.

Functions

This section is empty.

Types

type Cleanup

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

Cleanup is a scheduler.Executor that removes processed-event records older than the configured retention period. Runs daily at 04:00 by default.

It resolves the database through the tenant-aware getDB resolver and fans out across the configured tenants in (static) multi-tenant mode, because the scheduler builds the JobContext from a tenant-less context (shared with the outbox cleanup via multitenant.FanOutRetentionCleanup).

func (*Cleanup) Execute

func (c *Cleanup) Execute(jobCtx scheduler.JobContext) error

type HoldDrain added in v0.61.0

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

HoldDrain replays held messages, tenant by tenant, in the order they were parked. One pass leases a tenant, replays its rows oldest-first, and either releases the tenant when nothing remains or defers it behind a backoff.

Replays go through the streams lane, so the drain runs where the consumers do; the ledger is the control-plane database, so every replica sees the same holds and the lease is what stops them replaying the same tenant at once.

func (*HoldDrain) Execute added in v0.61.0

func (d *HoldDrain) Execute(jobCtx scheduler.JobContext) error

Execute runs one drain pass over every holding consumer.

type HoldRow added in v0.61.0

type HoldRow struct {
	Consumer   string
	Stream     string
	Offset     int64
	TenantID   string
	Data       []byte
	Properties []byte
	HeldAt     time.Time
}

HoldRow is one parked stream delivery. (Consumer, Stream, Offset) is its identity: a partition's offsets are unique within it, and a super stream's partition is its own stream.

type HoldStats added in v0.61.0

type HoldStats struct {
	Tenants         int64
	Rows            int64
	OldestHeldSince time.Time
}

HoldStats is what the gauges report for one consumer.

type HoldStore added in v0.61.0

type HoldStore interface {
	// Park inserts the row and marks its tenant held, in tx. It is idempotent on
	// the row's identity: a redelivery of an already-parked offset reports
	// inserted=false rather than failing.
	Park(ctx context.Context, tx dbtypes.Tx, row *HoldRow) (inserted bool, err error)

	// HeldTenants lists the tenants currently held for a consumer.
	HeldTenants(ctx context.Context, db dbtypes.Interface, consumer string) ([]string, error)

	// ListTenants returns every held tenant's full drain state, oldest first.
	ListTenants(ctx context.Context, db dbtypes.Interface, consumer string) ([]HoldTenant, error)

	// DueTenants lists held tenants whose next attempt has come and whose lease is
	// free, oldest first.
	DueTenants(ctx context.Context, db dbtypes.Interface, consumer string, limit int) ([]HoldTenant, error)

	// AcquireLease takes or renews the drain lease for one tenant, reporting false
	// when another owner holds a live one.
	AcquireLease(ctx context.Context, db dbtypes.Interface, consumer, tenant, owner string, lease time.Duration) (bool, error)

	// ReleaseLease drops a lease this owner holds.
	ReleaseLease(ctx context.Context, db dbtypes.Interface, consumer, tenant, owner string) error

	// NextRows returns the tenant's rows in (stream, offset) order.
	NextRows(ctx context.Context, db dbtypes.Interface, consumer, tenant string, limit int) ([]HoldRow, error)

	// DeleteRow removes one replayed row. Fenced by the lease: a write affecting no
	// rows means the lease was lost, and the caller discards the replay's outcome.
	DeleteRow(ctx context.Context, db dbtypes.Interface, consumer, stream string, offset int64, tenant, owner string) (deleted bool, err error)

	// Defer records a failed replay: one more attempt, the next one backed off,
	// the error bounded, the lease cleared. Fenced by the lease.
	Defer(ctx context.Context, db dbtypes.Interface, consumer, tenant, owner string, backoff time.Duration, lastErr string) (updated bool, err error)

	// Release deletes the tenant's marker, and only when no rows remain. Fenced by
	// the lease.
	Release(ctx context.Context, db dbtypes.Interface, consumer, tenant, owner string) (released bool, err error)

	// Stats reports what the gauges publish for one consumer.
	Stats(ctx context.Context, db dbtypes.Interface, consumer string) (HoldStats, error)

	// CreateTable creates both tables and their indexes if they do not exist.
	CreateTable(ctx context.Context, db dbtypes.Interface) error
}

HoldStore is the hold ledger's persistence. Every method takes the resolved control-plane database: a hold lives there and nowhere else, because a tenant whose own database is down cannot hold its own messages.

Database time is the clock throughout — NOW() on PostgreSQL, SYSTIMESTAMP on Oracle — so replicas with skewed clocks agree on when a lease expired and when a tenant is due.

func NewOracleHoldStore added in v0.61.0

func NewOracleHoldStore(tableName string) (HoldStore, error)

NewOracleHoldStore creates an Oracle hold store, refusing a table name whose derived names would not fit.

func NewPostgresHoldStore added in v0.61.0

func NewPostgresHoldStore(tableName string) (HoldStore, error)

NewPostgresHoldStore creates a PostgreSQL hold store, refusing a table name whose derived names would not fit.

type HoldTenant added in v0.61.0

type HoldTenant struct {
	Consumer      string
	TenantID      string
	HeldSince     time.Time
	Attempts      int
	NextAttemptAt time.Time
	LastError     string
}

HoldTenant is one held tenant's drain state.

type Inbox

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

Inbox implements app.InboxProcessor, backed by the module's lazily-initialized vendor store.

func (*Inbox) ProcessOnce

func (i *Inbox) ProcessOnce(ctx context.Context, eventID string, fn func(ctx context.Context, tx dbtypes.Tx) error) error

ProcessOnce records eventID in the ledger and runs fn exactly once per id, atomically within a single transaction. A redelivery of an already-processed id short-circuits (fn is not run) and returns nil. The tenant is resolved from ctx; in single-tenant mode the tenant id is empty.

type Module

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

Module implements the GoBricks Module interface for the consumer-side inbox. It provides durable, exactly-once event processing via deps.Inbox.ProcessOnce and a daily cleanup job that prunes old processed-event records.

Register it like any other module (the scheduler is optional but required for the retention cleanup job):

for _, m := range []app.Module{
    scheduler.NewModule(), // optional: enables inbox-cleanup
    inbox.NewModule(),
    &myapp.ConsumerModule{},
} {
    if err := fw.RegisterModule(m); err != nil {
        log.Fatal(err)
    }
}

func NewModule

func NewModule() *Module

NewModule creates a new inbox Module instance.

func (*Module) HoldLedger added in v0.61.0

func (m *Module) HoldLedger() app.HoldLedger

HoldLedger is the port the streams lane parks through, or nil when this module runs no hold. The lane reads a nil as "no hold configured" and refuses any consumer that declared one, so the two answers must not be confused.

func (*Module) InboxProcessor

func (m *Module) InboxProcessor() app.InboxProcessor

InboxProcessor implements app.InboxProvider — returns the processor for ModuleDeps wiring. Returns nil when the inbox is disabled.

func (*Module) Init

func (m *Module) Init(deps *app.ModuleDeps) error

Init implements app.Module.

func (*Module) Name

func (m *Module) Name() string

Name implements app.Module.

func (*Module) RegisterJobs

func (m *Module) RegisterJobs(registrar app.JobRegistrar) error

RegisterJobs implements app.JobProvider. The inbox has no relay; it registers only the retention cleanup job, and only when retention is positive.

func (*Module) SetHoldReplayer added in v0.61.0

func (m *Module) SetHoldReplayer(src func() app.HoldReplayer)

SetHoldReplayer receives the source of the replayer the drain drives. It is a func rather than the value because the streams manager does not exist yet when modules are registered.

func (*Module) SetSharedResolvers added in v0.54.0

func (m *Module) SetSharedResolvers(
	db func(context.Context) (dbtypes.Interface, error),
	_ func(context.Context) (messaging.AMQPClient, error),
)

SetSharedResolvers injects the control-plane ("" key) resolvers. Called by app.RegisterModule; used only when inbox.tenancy=shared. The messaging resolver is ignored — the inbox has no broker of its own (ProcessOnce only touches the database).

func (*Module) Shutdown

func (m *Module) Shutdown() error

Shutdown implements app.Module.

type Record

type Record struct {
	TenantID    string
	EventID     string
	ProcessedAt time.Time
}

Record is a single row in the inbox ledger: a processed event id scoped to a tenant, with the time it was processed.

type Store

type Store interface {
	// MarkProcessed records (tenant_id, event_id) within the given transaction.
	// It returns inserted=true the first time an id is seen and inserted=false on
	// a duplicate (the id was already processed).
	MarkProcessed(ctx context.Context, tx dbtypes.Tx, rec Record) (inserted bool, err error)

	// DeleteProcessed removes ledger rows processed before the given time.
	// Returns the number of rows deleted.
	DeleteProcessed(ctx context.Context, db dbtypes.Interface, before time.Time) (int64, error)

	// CreateTable creates the inbox table and its index if they do not exist.
	// Used for auto-migration when inbox.autocreatetable is true.
	CreateTable(ctx context.Context, db dbtypes.Interface) error
}

Store abstracts inbox ledger operations for vendor-agnostic SQL. Implementations exist for PostgreSQL and Oracle with vendor-specific placeholder styles, DDL, and duplicate-detection (PostgreSQL ON CONFLICT vs Oracle unique-violation catch).

func NewOracleStore

func NewOracleStore(tableName string) (Store, error)

NewOracleStore creates a new Oracle inbox store. Returns an error if the table name is not a safe, unqualified identifier.

func NewPostgresStore

func NewPostgresStore(tableName string) (Store, error)

NewPostgresStore creates a new PostgreSQL inbox store. Returns an error if the table name is not a safe, unqualified identifier.

Directories

Path Synopsis
Package testing provides test utilities for the consumer-side inbox.
Package testing provides test utilities for the consumer-side inbox.

Jump to

Keyboard shortcuts

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