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
- type Cleanup
- type HoldDrain
- type HoldRow
- type HoldStats
- type HoldStore
- type HoldTenant
- type Inbox
- type Module
- func (m *Module) HoldLedger() app.HoldLedger
- func (m *Module) InboxProcessor() app.InboxProcessor
- func (m *Module) Init(deps *app.ModuleDeps) error
- func (m *Module) Name() string
- func (m *Module) RegisterJobs(registrar app.JobRegistrar) error
- func (m *Module) SetHoldReplayer(src func() app.HoldReplayer)
- func (m *Module) SetSharedResolvers(db func(context.Context) (dbtypes.Interface, error), ...)
- func (m *Module) Shutdown() error
- type Record
- type Store
Constants ¶
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.
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.
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).
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.
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 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
NewOracleHoldStore creates an Oracle hold store, refusing a table name whose derived names would not fit.
func NewPostgresHoldStore ¶ added in v0.61.0
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 (*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) 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).
type Record ¶
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 ¶
NewOracleStore creates a new Oracle inbox store. Returns an error if the table name is not a safe, unqualified identifier.
func NewPostgresStore ¶
NewPostgresStore creates a new PostgreSQL inbox store. Returns an error if the table name is not a safe, unqualified identifier.